diff --git a/.github/workflows/guards-nightly.yml b/.github/workflows/guards-nightly.yml index dfbcdce6de..5e7f1acdce 100644 --- a/.github/workflows/guards-nightly.yml +++ b/.github/workflows/guards-nightly.yml @@ -181,6 +181,13 @@ jobs: run: setsid --wait bash scripts/tests/compute_row_zero_tests_test.sh --mutants - name: shared-checkout shallow lock case table and mutants (#5025, LAB) run: setsid --wait bash scripts/shared_checkout_no_shallow.sh --mutants + - name: Parity-receipt count equals EXPECTED_RECEIPTS (verify mode, #5502, LAB) + id: lab_parity_receipts + if: ${{ !cancelled() }} + run: | + set -euo pipefail + exec > >(tee -a "$RUNNER_TEMP/parity-receipts.log") 2>&1 + setsid --wait bash scripts/parity_receipt_denominator.sh - name: nextest list-wrapper case table (#5298, LAB) run: setsid --wait bash scripts/ci/nextest_list_wrapper.sh --self-test - name: affected_set.sh case table and mutants (FLOW-010 E5, LAB) @@ -429,6 +436,36 @@ jobs: set -euo pipefail bash scripts/lab_ticket.sh --label lab-affected-readers --title "LAB: nightly affected-set guards red (#5809)" \ --owner aprender-w5018 --log "$RUNNER_TEMP/affected.log" --event "$GITHUB_EVENT_NAME" --run-url "$RUN_URL" + # #5803/#5804: the moved tuner tests must still be listed by cargo exactly once, none ignored. LAB, never on a PR path (C324): + # it invokes cargo, so it runs only in this night job; the moved tests themselves run from ci/explicit-test-commands.d 295 and 298. + - name: Tuner test-move guard (#5803) + id: lab_tuner_test_move + if: ${{ !cancelled() }} + run: | + set -euo pipefail + exec > >(tee -a "$RUNNER_TEMP/tuner-move.log") 2>&1 + setsid --wait bash scripts/check_tuner_test_move.sh --self-test + setsid --wait bash scripts/check_tuner_test_move.sh + - name: A red tuner-test-move night opens or updates its one LAB ticket (#5803) + if: failure() && steps.lab_tuner_test_move.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + bash scripts/lab_ticket.sh --label lab-tuner-test-move --title "LAB: nightly tuner test-move guard red (#5803)" \ + --owner aprender-w5018 --log "$RUNNER_TEMP/tuner-move.log" --event "$GITHUB_EVENT_NAME" --run-url "$RUN_URL" + - name: A red parity-receipt night opens or updates its one LAB ticket (#5502) + if: failure() && steps.lab_parity_receipts.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + bash scripts/lab_ticket.sh --label lab-parity-receipts --title "LAB: parity-receipt count differs from EXPECTED_RECEIPTS (#5502)" \ + --owner aprender-w5018 --log "$RUNNER_TEMP/parity-receipts.log" --event "$GITHUB_EVENT_NAME" --run-url "$RUN_URL" - name: A red census night opens or updates the one LAB ticket (#4709) if: failure() && steps.ratchet_census.outcome == 'failure' env: diff --git a/Cargo.lock b/Cargo.lock index 0ccb3d8ed7..100fcb42fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -451,9 +451,8 @@ name = "aprender-compute" version = "0.71.3" dependencies = [ "anyhow", - "aprender-contracts", + "aprender-contracts-key", "aprender-contracts-macros", - "aprender-core", "aprender-cuda-edge", "aprender-gemm-codegen", "aprender-gpu", @@ -514,6 +513,7 @@ dependencies = [ name = "aprender-contracts" version = "0.71.3" dependencies = [ + "aprender-contracts-key", "aprender-contracts-macros", "blake3", "criterion 0.5.1", @@ -546,6 +546,10 @@ dependencies = [ "tempfile", ] +[[package]] +name = "aprender-contracts-key" +version = "0.71.3" + [[package]] name = "aprender-contracts-macros" version = "0.71.3" @@ -581,6 +585,7 @@ dependencies = [ "aprender-common", "aprender-compute", "aprender-contracts", + "aprender-contracts-key", "aprender-contracts-macros", "aprender-data", "aprender-profile", @@ -707,7 +712,7 @@ dependencies = [ "aprender-build-sha", "aprender-common", "aprender-compute", - "aprender-contracts", + "aprender-contracts-key", "arrow 59.2.0", "axum 0.7.9", "bytemuck", @@ -920,6 +925,7 @@ dependencies = [ "aprender-common", "aprender-compute", "aprender-contracts", + "aprender-contracts-key", "aprender-contracts-macros", "aprender-core", "aprender-cuda-edge", @@ -986,7 +992,7 @@ name = "aprender-present-cli" version = "0.71.3" dependencies = [ "aprender-build-sha", - "aprender-contracts", + "aprender-contracts-key", "aprender-present-yaml", "clap", "notify", @@ -1000,7 +1006,7 @@ dependencies = [ name = "aprender-present-core" version = "0.71.3" dependencies = [ - "aprender-contracts", + "aprender-contracts-key", "aprender-contracts-macros", "criterion 0.7.0", "proptest", @@ -1293,7 +1299,7 @@ version = "0.71.3" dependencies = [ "aprender-common", "aprender-compute", - "aprender-contracts", + "aprender-contracts-key", "aprender-serve", "async-trait", "bincode", @@ -1489,6 +1495,7 @@ version = "0.71.3" dependencies = [ "aprender-build-sha", "aprender-contracts", + "aprender-contracts-key", "aprender-contracts-macros", "aprender-present-core", "aprender-present-terminal", @@ -1680,6 +1687,7 @@ dependencies = [ "aprender-common", "aprender-compute", "aprender-contracts", + "aprender-contracts-key", "aprender-contracts-macros", "aprender-core", "aprender-data", diff --git a/Cargo.toml b/Cargo.toml index 71b08df5c4..f811e5a87e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ # --- APR-MONO Phase 2b: was provable-contracts --- "crates/aprender-contracts", "crates/aprender-contracts-macros", + "crates/aprender-contracts-key", "crates/aprender-contracts-cli", # EPIC #4232: shared update check + ` update` for every sovereign binary. "crates/aprender-update", @@ -321,6 +322,7 @@ aprender-train-common = { path = "crates/aprender-train-common", version = "0.71 aprender-train-distill = { path = "crates/aprender-train-distill", version = "0.71.3" } aprender-orchestrate = { path = "crates/aprender-orchestrate", version = "0.71.3" } aprender-contracts = { path = "crates/aprender-contracts", version = "0.71.3" } +aprender-contracts-key = { path = "crates/aprender-contracts-key", version = "0.71.3" } aprender-build-sha = { path = "crates/aprender-build-sha", version = "0.71.3" } aprender-rollup = { path = "crates/aprender-rollup", version = "0.71.3" } aprender-profile = { path = "crates/aprender-profile", version = "0.71.3" } diff --git a/README.md b/README.md index 278745b91a..e5d819f98e 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ publishing — all backed by YAML provable contracts that fail CI on drift. | Metric | Count | Source of truth | |-------:|------:|---| | Workspace crates | **81** workspace crates | `cargo metadata --no-deps` (NOT `ls crates/` — 4 are `exclude`d, 1 has no Cargo.toml) | -| Provable contracts | **2005** provable contracts | `contracts/census.json` `.n_files` — the set `pv lint` walks (`pv census`, ONT-001 ONT-1; regenerated by `make contracts`, written by `make readme-sync`, guarded by `scripts/check_readme_claims.sh`) | +| Provable contracts | **2006** provable contracts | `contracts/census.json` `.n_files` — the set `pv lint` walks (`pv census`, ONT-001 ONT-1; regenerated by `make contracts`, written by `make readme-sync`, guarded by `scripts/check_readme_claims.sh`) | | CLI commands | **113** CLI commands | `contracts/apr-cli-commands-v1.yaml` §`commands` (parse the list; `apr --help` also lists `help` itself, and `grep -c '^ - name:'` gives 119 — other same-indent `name:` keys exist in the file) | | Book CLI chapters | **114** chapters | `ls book/src/cli/*.md` | | Book lib chapters | **73** chapters | `ls book/src/lib/*.md` (parity with `pub mod`) | @@ -403,7 +403,7 @@ falsification_tests: prediction: apr validate bad-model.apr exits non-zero ``` -The tree carries 2005 contracts across inference, training, quantization, attention, FFN, +The tree carries 2006 contracts across inference, training, quantization, attention, FFN, tokenization, model formats, CLI safety — and this README itself. ## Migration from old crates diff --git a/ci/explicit-test-commands.d/295-aprender-core-tuner-training-data-quality.cmd b/ci/explicit-test-commands.d/295-aprender-core-tuner-training-data-quality.cmd new file mode 100644 index 0000000000..c69214adf3 --- /dev/null +++ b/ci/explicit-test-commands.d/295-aprender-core-tuner-training-data-quality.cmd @@ -0,0 +1 @@ +cargo test -p aprender-core --test tuner_training_data_quality diff --git a/ci/explicit-test-commands.d/298-aprender-compute-tuner-falsification-5803.cmd b/ci/explicit-test-commands.d/298-aprender-compute-tuner-falsification-5803.cmd new file mode 100644 index 0000000000..32265c8925 --- /dev/null +++ b/ci/explicit-test-commands.d/298-aprender-compute-tuner-falsification-5803.cmd @@ -0,0 +1 @@ +cargo test -p aprender-compute --test tuner_falsification diff --git a/contracts/contracts.nt b/contracts/contracts.nt index 49ac2468bb..ec2f488990 100644 --- a/contracts/contracts.nt +++ b/contracts/contracts.nt @@ -9774,6 +9774,13 @@ "course-flow-batch-wip-v1"^^ . "course-flow-batch-wip"^^ . "1.0.0"^^ . + . + . + . + "contracts/course-flow-content-factory-v2-v1.yaml"^^ . + "course-flow-content-factory-v2-v1"^^ . + "course-flow-content-factory-v2"^^ . + "1.0.0"^^ . . . . diff --git a/contracts/course-flow-content-factory-v2-v1.yaml b/contracts/course-flow-content-factory-v2-v1.yaml new file mode 100644 index 0000000000..b1641054ab --- /dev/null +++ b/contracts/course-flow-content-factory-v2-v1.yaml @@ -0,0 +1,66 @@ +metadata: + version: "1.0.0" + created: "2026-10-19" + author: PAIML Engineering + registry: true + references: + - "flow model spec, internal (eqs 1 and 2)" + - "FLOW-007 row B5 (#5557)" + description: "course_flow models Content Factory v2: three-platform fan-out, rework loop, trunk-first GPU rule, LinkedIn renders, work vs elapsed build time" + +name: course-flow-content-factory-v2 +version: "1.0.0" +scope: "crates/aprender-simulate/src/scenarios/course_flow.rs" + +description: | + The factory-v2 case sends every master to edX, Coursera and LinkedIn with no split. + A build repeats with probability alpha, so the mean build work is B/(1-alpha). While the + running mean lead exceeds trunk_first_w_d days the GPU serves courses before Shorts. + LinkedIn adds li_k + 4 renders per master on the GPU. The result keeps build work + (sizing, eq 2) apart from build elapsed time (lead time, eq 1). + +equations: + coursera_stage_load: + description: "Coursera stage busy fraction when every master takes the stage (the Chrome lanes also carry the Shorts stage, which the test adds, about 0.87 points)" + formula: "busy = starts_per_day * stage_min / (chrome_lanes * 1440) = 0.625, plus the Shorts stage load" + + rework_build_work: + description: "mean build work per course under the rework loop" + formula: "B' = B / (1 - alpha)" + +falsification_tests: + - id: FALSIFY-001 + description: "Coursera stage busy matches the law within 2 points" + test: "cargo test -p aprender-simulate --lib coursera_stage_matches_the_law_within_two_points" + prediction: "the test passes" + if_fails: "the fan-out does not send every master to the Coursera stage" + + - id: FALSIFY-002 + description: "at alpha 0.16 the build stage is busy at the law's 94.7% within two points" + test: "cargo test -p aprender-simulate --lib build_busy_at_alpha_016_matches_the_law" + prediction: "the test passes" + if_fails: "the rework loop is missing or mis-sized" + + - id: FALSIFY-003 + description: "mean build work is B/(1-alpha)" + test: "cargo test -p aprender-simulate --lib rework_loop_scales_build_work_by_one_over_one_minus_alpha" + prediction: "the test passes" + if_fails: "the rework loop does not scale build work by 1/(1-alpha)" + + - id: FALSIFY-004 + description: "build elapsed time exceeds build work (they are reported apart)" + test: "cargo test -p aprender-simulate --lib build_elapsed_is_not_the_build_work" + prediction: "the test passes" + if_fails: "work and elapsed build time are conflated" + + - id: FALSIFY-005 + description: "the trunk-first rule is not a no-op: the rule fires only on the render stage, with a positive threshold, after a course finished and while the mean lead exceeds it, and turning it on changes the run (LinkedIn renders are covered by linkedin_renders_load_the_gpu, not by this test)" + test: "cargo test -p aprender-simulate --lib trunk_first_rule_changes_the_outcome" + prediction: "the test passes" + if_fails: "the GPU priority rule is dead" + + - id: FALSIFY-006 + description: "alpha is a probability below 1 (1 or more would repeat a build forever) and li_k is bounded to [0, 64] (li_k + 4 renders per master)" + test: "cargo test -p aprender-simulate --lib alpha_must_be_below_one" + prediction: "the test passes" + if_fails: "apply_course_inputs accepts an alpha that never terminates" diff --git a/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json b/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json index 5b01d0e1b1..bf2f76b233 100644 --- a/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json +++ b/contracts/witness/811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850.json @@ -1,5 +1,5 @@ { - "census_id_set_sha256": "e01a28975ffa19f5a1ed1edd3f59d7eacd73a80decd44c723be4fe5dd9b53e7e", + "census_id_set_sha256": "4039ff1d878c323278405342882da062310a234a5674cf56a0086a75015eb880", "relations_sha256": "811c78184eb2609244c59534f015f0be2f90923120a3027c176ad56723d0d850", "checkable_n": 87, "result": { diff --git a/crates/aprender-compute/Cargo.toml b/crates/aprender-compute/Cargo.toml index 1aa9e9f4ab..0c7fcfa8c0 100644 --- a/crates/aprender-compute/Cargo.toml +++ b/crates/aprender-compute/Cargo.toml @@ -63,11 +63,9 @@ anyhow = "1.0.100" # Hardware capability detection (PMAT-447) chrono = "0.4" toml = "0.8" -# ML tuner integration (SHOWCASE-BRICK-001, Section 12) — aprender RandomForest is a -# DEV-dependency only: the SIMD foundation must not depend on the ML layer (layer -# inversion). The `ml-tuner` showcase lives in examples/ml_tuner_demo.rs + the tuner -# falsification tests, which consume aprender directly. Dev-dep path cycles are legal -# in cargo and dropped on publish, keeping the workspace a self-contained DAG (APR-MONO). +# ML tuner integration (SHOWCASE-BRICK-001, Section 12) — the SIMD foundation takes no +# dependency, not even a dev one, on the ML layer. The aprender RandomForest tests live in +# crates/aprender-core/tests/tuner_training_data_quality.rs (FLOW-010, #5803). # NOTE: trueno-graph (=aprender-graph) removed (APR-MONO self-containment): aprender-graph # depends on trueno, so compute→graph closed a cycle. The `execution-graph` feature's only # trueno-graph use was the thin `to_csr()`/`graph_to_csr()` CsrGraph exporters; CSR export @@ -84,7 +82,7 @@ web-sys = { version = "0.3", optional = true, features = ["console"] } hostname = "0.4" [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) serde = { version = "1", features = ["derive"] } serde-saphyr = { workspace = true } @@ -106,9 +104,6 @@ trueno-cuda-edge = { workspace = true } # Sparse and solver crates for contract tests trueno-sparse = { workspace = true } trueno-solve = { workspace = true } -# ML-tuner showcase (SHOWCASE-BRICK-001): aprender RandomForest, DEV-only to avoid the -# compute→core layer inversion. Path dev-dep cycle is legal + dropped on publish. -aprender = { path = "../aprender-core", package = "aprender-core", default-features = false } ndarray = "0.17.2" faer = "0.24" # For GEMM benchmark comparison (fastest pure-Rust BLAS) matrixmultiply = "0.3" # Direct matrixmultiply crate benchmark (engine behind ndarray) @@ -139,9 +134,8 @@ hardware-detect = ["dep:dirs"] tui-monitor = ["presentar-core", "presentar-terminal", "crossterm", "trueno-gpu", "dep:tracing", "tracing-subscriber", "tracing-appender", "dep:dirs"] # CUDA support for TUI (real hardware monitoring) cuda = ["trueno-gpu/cuda"] -# ML-based kernel selection and throughput prediction (SHOWCASE-BRICK-001) -# Showcase: feed trueno tuner features into aprender RandomForest (example + tests only; -# aprender is a dev-dependency, so this feature activates no lib dependency). +# DEPRECATED no-op, kept so removing it is not a semver break for 0.71.x users (activates nothing; +# the aprender RandomForest tests moved to aprender-core, #5803). ml-tuner = [] # NOTE: `execution-graph` feature removed (APR-MONO self-containment) — its only purpose # was the trueno-graph CsrGraph exporters (see above). The exec-graph profiler itself diff --git a/crates/aprender-compute/README.md b/crates/aprender-compute/README.md index a462ad3d5e..50743d1f6e 100644 --- a/crates/aprender-compute/README.md +++ b/crates/aprender-compute/README.md @@ -222,7 +222,6 @@ GEMV, Quantization (Q4K/Q5K/Q6K). JIT disk cache at | `parallel` | Rayon parallel iterators | | `cuda-monitor` | NVIDIA device info via trueno-gpu | | `cuda` | Full CUDA support via trueno-gpu | -| `ml-tuner` | ML-based kernel selection via aprender RandomForest | | `execution-graph` | PTX-to-kernel call graph profiling | | `tui-monitor` | Terminal monitoring dashboard | | `hardware-detect` | Hardware capability detection and caching | diff --git a/crates/aprender-compute/book/src/ecosystem/aprender.md b/crates/aprender-compute/book/src/ecosystem/aprender.md index bd8a8058fd..9d82ed5d31 100644 --- a/crates/aprender-compute/book/src/ecosystem/aprender.md +++ b/crates/aprender-compute/book/src/ecosystem/aprender.md @@ -10,51 +10,12 @@ The integration provides: - **RandomForestClassifier** for kernel selection - Training on benchmark data for hardware-specific optimization -## Enabling the Integration +## Where the Integration Lives -Add the `ml-tuner` feature to your `Cargo.toml`: - -```toml -[dependencies] -trueno = { version = "0.13", features = ["ml-tuner"] } -``` - -## Feature Matrix - -| Feature | Default | ml-tuner | -|---------|---------|----------| -| TunerFeatures (42-dim) | Yes | Yes | -| Heuristic prediction | Yes | Yes | -| Roofline clamping | Yes | Yes | -| RandomForest regressor | No | Yes | -| RandomForest classifier | No | Yes | -| Custom model training | No | Yes | - -## Usage Example - -```rust -use trueno::tuner::{ThroughputRegressor, TunerFeatures, QuantType}; - -// Create RF-backed regressor -let mut regressor = ThroughputRegressor::with_random_forest(100); - -// Collect benchmark data -let training_data: Vec<(TunerFeatures, f32)> = collect_benchmarks(); - -// Train the model -regressor.train_random_forest(&training_data)?; - -// Use trained model for predictions -let features = TunerFeatures::builder() - .model_params_b(7.0) - .batch_size(4) - .quant_type(QuantType::Q4K) - .gpu_mem_bw_gbs(1000.0) - .build(); - -let pred = regressor.predict(&features); -println!("Predicted throughput: {:.1} tok/s", pred.predicted_tps); -``` +The `ml-tuner` feature was removed (FLOW-010, #5803): trueno has no dependency on +aprender, so the SIMD foundation stays below the ML layer. The RandomForest showcase +runs from aprender-core's tests (`crates/aprender-core/tests/tuner_training_data_quality.rs`), +which depend on trueno, not the reverse. trueno keeps the heuristic models. ## Why aprender? @@ -119,7 +80,6 @@ let model_json = fs::read_to_string("throughput_model.json")?; let regressor: ThroughputRegressor = serde_json::from_str(&model_json)?; ``` -**Note:** RandomForest models are not serialized (marked `#[serde(skip)]`). After loading, you must retrain or use heuristic fallback. ## Further Reading diff --git a/crates/aprender-compute/book/src/performance/ml-tuner.md b/crates/aprender-compute/book/src/performance/ml-tuner.md index 2c1074284d..e72b374070 100644 --- a/crates/aprender-compute/book/src/performance/ml-tuner.md +++ b/crates/aprender-compute/book/src/performance/ml-tuner.md @@ -82,72 +82,15 @@ for (kernel, conf) in recommendation.alternatives.iter().take(3) { | M=2-3 | VectorizedQ4K | Low batch, vectorized still efficient | | M>=4 | BatchedQ4K | High batch, batched attention wins | -## RandomForest Models (Optional) +## RandomForest Models (Relocated) -With the `ml-tuner` feature, you can use aprender's RandomForest models for learned optimization: - -```toml -# Cargo.toml -[dependencies] -trueno = { version = "0.13", features = ["ml-tuner"] } -``` - -### Training a Custom Regressor - -```rust -use trueno::tuner::{ThroughputRegressor, TunerFeatures, QuantType}; - -// Create RF-backed regressor with 100 trees -let mut regressor = ThroughputRegressor::with_random_forest(100); - -// Generate training data from benchmarks -let training_data: Vec<(TunerFeatures, f32)> = (0..100) - .map(|i| { - let batch = 1 + (i % 8) as u32; - let features = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .quant_type(QuantType::Q4K) - .gpu_mem_bw_gbs(1000.0) - .cuda_graphs(batch == 1) - .build(); - // Measured throughput from benchmark - let throughput = 200.0 + (batch as f32) * 80.0; - (features, throughput) - }) - .collect(); - -// Train the model -regressor.train_random_forest(&training_data)?; - -// Predictions now use learned model -let pred = regressor.predict(&features); -println!("RF prediction: {:.1} tok/s", pred.predicted_tps); -``` - -### Training a Custom Classifier - -```rust -use trueno::tuner::{KernelClassifier, TunerFeatures, QuantType}; - -let mut classifier = KernelClassifier::with_random_forest(50); - -// Label encoding: VectorizedQ4K=2, BatchedQ4K=3 -let training_data: Vec<(TunerFeatures, u32)> = (0..100) - .map(|i| { - let batch = 1 + (i % 8) as u32; - let features = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .quant_type(QuantType::Q4K) - .build(); - let label = if batch >= 4 { 3 } else { 2 }; - (features, label) - }) - .collect(); - -classifier.train(&training_data)?; -``` +The `ml-tuner` feature is now a deprecated no-op (`ml-tuner = []`, kept so removing it +is not a semver break), and the `with_random_forest` / `train_random_forest` / +`KernelClassifier::train` wrappers were removed (FLOW-010, #5803): the SIMD compute +crate must not depend on the ML layer. The aprender RandomForest showcase now lives in +`crates/aprender-core/tests/tuner_training_data_quality.rs`, which feeds `TunerFeatures` +into `aprender::RandomForestRegressor` / `RandomForestClassifier`. This crate keeps the +heuristic `ThroughputRegressor` and `KernelClassifier`. ## Full Tuner Recommendations @@ -178,11 +121,8 @@ for exp in &rec.suggested_experiments { ## Running the Demo ```bash -# Default (heuristic models) +# Heuristic models cargo run --example ml_tuner_demo - -# With RandomForest models -cargo run --example ml_tuner_demo --features ml-tuner ``` ## Integration with ComputeBrick diff --git a/crates/aprender-compute/build.rs b/crates/aprender-compute/build.rs index 5c02ae8a78..14c0a7d60e 100644 --- a/crates/aprender-compute/build.rs +++ b/crates/aprender-compute/build.rs @@ -36,7 +36,7 @@ struct Binding { /// Convert a contract filename + equation into a canonical env var name. fn env_var_name(contract: &str, equation: &str) -> String { - provable_contracts::build_helper::env_key( + aprender_contracts_key::env_key( contract.trim_end_matches(".yaml").trim_end_matches(".yml"), equation, ) diff --git a/crates/aprender-compute/docs/prompts/95_percent_mandate.md b/crates/aprender-compute/docs/prompts/95_percent_mandate.md index 2f5309db32..92697b0b55 100644 --- a/crates/aprender-compute/docs/prompts/95_percent_mandate.md +++ b/crates/aprender-compute/docs/prompts/95_percent_mandate.md @@ -18,7 +18,7 @@ --package trueno --package trueno-gpu \ --lib \ --no-default-features \ - --features "parallel,cuda,wgpu,ml-tuner,execution-graph,hardware-detect,tracing" \ + --features "parallel,cuda,wgpu,execution-graph,hardware-detect,tracing" \ --html ``` 2. **Close the Gaps**: diff --git a/crates/aprender-compute/examples/README.md b/crates/aprender-compute/examples/README.md index 3dba4e73d9..cf6dc74bc4 100644 --- a/crates/aprender-compute/examples/README.md +++ b/crates/aprender-compute/examples/README.md @@ -25,8 +25,8 @@ This directory contains examples demonstrating Trueno's high-performance compute | `vocab_bench.rs` | Vocabulary processing benchmark | `cargo run --release --example vocab_bench` | | `profile_vocab.rs` | Vocabulary profiling | `cargo run --release --example profile_vocab` | | `execution_graph.rs` | Execution path graph demo | `cargo run --release --features execution-graph --example execution_graph` | -| `ml_tuner_demo.rs` | ML-based kernel selection | `cargo run --release --features ml-tuner --example ml_tuner_demo` | -| `ml_tuner_evolution.rs` | ML tuner evolution demo | `cargo run --release --features ml-tuner --example ml_tuner_evolution` | +| `ml_tuner_demo.rs` | Heuristic kernel selection and throughput prediction | `cargo run --release --example ml_tuner_demo` | +| `ml_tuner_evolution.rs` | ML tuner evolution demo | `cargo run --release --features hardware-detect --example ml_tuner_evolution` | | `model_tracing.rs` | Model-level inference tracing | `cargo run --release --example model_tracing` | | `tiling_demo.rs` | Tiling compute blocks | `cargo run --release --example tiling_demo` | | `tile_profiler_demo.rs` | Tile profiler demo | `cargo run --release --example tile_profiler_demo` | diff --git a/crates/aprender-compute/examples/ml_tuner_demo.rs b/crates/aprender-compute/examples/ml_tuner_demo.rs index b176d6e348..6adb5f46f7 100644 --- a/crates/aprender-compute/examples/ml_tuner_demo.rs +++ b/crates/aprender-compute/examples/ml_tuner_demo.rs @@ -6,9 +6,6 @@ //! //! Run with default (heuristic models): //! cargo run --example ml_tuner_demo -//! -//! Run with RandomForest models (requires ml-tuner feature): -//! cargo run --example ml_tuner_demo --features ml-tuner use trueno::tuner::{BrickTuner, KernelClassifier, QuantType, ThroughputRegressor, TunerFeatures}; @@ -163,101 +160,15 @@ fn demo_full_tuner(features: &TunerFeatures) { println!(); } -/// Section 6: RandomForest models (ml-tuner showcase, SHOWCASE-BRICK-001). -/// -/// The SIMD `trueno` tuner produces `TunerFeatures` vectors; this section feeds them -/// directly into `aprender::RandomForestRegressor`/`RandomForestClassifier`. aprender is -/// a **dev-dependency** of `aprender-compute`, so the compute lib itself never depends on -/// the ML layer — the showcase lives entirely here at the example layer, above both. +/// Section 6: RandomForest models (SHOWCASE-BRICK-001). The aprender half of the showcase +/// is tested in `crates/aprender-core/tests/tuner_training_data_quality.rs`: compute takes +/// no dependency on the ML layer, not even a dev one (FLOW-010, #5803). fn demo_random_forest(features: &TunerFeatures) { - #[cfg(feature = "ml-tuner")] - { - use aprender::primitives::{Matrix, Vector}; - use aprender::tree::{RandomForestClassifier, RandomForestRegressor}; - - println!("6. RandomForest Models (ml-tuner showcase)"); - println!(" --------------------------------------"); - - let dim = TunerFeatures::DIM; - let n = 100usize; - - // Regressor: trueno features -> predicted throughput (tok/s). - let mut xr = Vec::with_capacity(n * dim); - let mut yr = Vec::with_capacity(n); - for i in 0..n { - let batch = 1 + (i % 8) as u32; - let f = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .quant_type(QuantType::Q4K) - .gpu_mem_bw_gbs(1000.0) - .cuda_graphs(batch == 1) - .build(); - xr.extend(f.to_vector()); - yr.push(200.0 + (batch as f32) * 80.0 + (i as f32 * 0.5)); - } - println!(" Generated {n} regression samples"); - match Matrix::from_vec(n, dim, xr) { - Ok(x_mat) => { - let mut rf = RandomForestRegressor::new(100); - match rf.fit(&x_mat, &Vector::from_vec(yr)) { - Ok(()) => { - let fx = Matrix::from_vec(1, dim, features.to_vector().to_vec()) - .expect("single-row feature matrix"); - let pred = rf.predict(&fx); - println!( - " RF regressor: SUCCESS — M=4 prediction {:.1} tok/s", - pred.as_slice()[0] - ); - } - Err(e) => println!(" RF regressor: FAILED - {e}"), - } - } - Err(e) => println!(" RF regressor matrix: FAILED - {e}"), - } - - // Classifier: trueno features -> kernel-type label (3=Batched, 2=Vectorized). - let mut xc = Vec::with_capacity(n * dim); - let mut yc: Vec = Vec::with_capacity(n); - for i in 0..n { - let batch = 1 + (i % 8) as u32; - let f = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .quant_type(QuantType::Q4K) - .build(); - xc.extend(f.to_vector()); - yc.push(if batch >= 4 { 3 } else { 2 }); - } - match Matrix::from_vec(n, dim, xc) { - Ok(xc_mat) => { - let mut rf = RandomForestClassifier::new(50); - match rf.fit(&xc_mat, &yc) { - Ok(()) => { - let preds = rf.predict(&xc_mat); - let correct = - yc.iter().zip(preds.iter()).filter(|(a, b)| **a == **b).count(); - println!( - " RF classifier: SUCCESS — train accuracy {:.1}%", - correct as f32 / n as f32 * 100.0 - ); - } - Err(e) => println!(" RF classifier: FAILED - {e}"), - } - } - Err(e) => println!(" RF classifier matrix: FAILED - {e}"), - } - println!(); - } - - #[cfg(not(feature = "ml-tuner"))] - { - let _ = features; - println!("6. RandomForest Models"); - println!(" --------------------"); - println!(" [Disabled - enable with: --features ml-tuner]"); - println!(); - } + let _ = features; + println!("6. RandomForest Models"); + println!(" --------------------"); + println!(" [See aprender-core tests/tuner_training_data_quality.rs]"); + println!(); } fn main() { @@ -279,7 +190,7 @@ fn main() { println!(" - TunerFeatures: 42-dimension vector for ML models"); println!(" - Throughput prediction with roofline clamping (v1.1.0)"); println!(" - Kernel selection: BatchedQ4K for M>=4, VectorizedQ4K otherwise"); - println!(" - RandomForest available with --features ml-tuner"); + println!(" - RandomForest showcase: aprender-core tests/tuner_training_data_quality.rs"); println!(); println!("Next steps:"); println!(" cargo run --example quickstart # Basic trueno usage"); diff --git a/crates/aprender-compute/src/tuner/error.rs b/crates/aprender-compute/src/tuner/error.rs index bb9fd2e631..bb775e3900 100644 --- a/crates/aprender-compute/src/tuner/error.rs +++ b/crates/aprender-compute/src/tuner/error.rs @@ -15,7 +15,7 @@ pub enum TunerError { ModelNotFound, /// Prediction failed PredictionFailed(String), - /// Training failed (ml-tuner feature) + /// Training failed TrainingFailed(String), /// I/O error (file operations) Io(String), diff --git a/crates/aprender-compute/src/tuner/models/kernel.rs b/crates/aprender-compute/src/tuner/models/kernel.rs index 019b9037ef..0e392bbf49 100644 --- a/crates/aprender-compute/src/tuner/models/kernel.rs +++ b/crates/aprender-compute/src/tuner/models/kernel.rs @@ -9,10 +9,10 @@ use super::KernelRecommendation; /// Kernel classifier using simple rule-based logic. /// -/// The `ml-tuner` showcase (SHOWCASE-BRICK-001) trains an +/// The RandomForest showcase (SHOWCASE-BRICK-001) trains an /// `aprender::RandomForestClassifier` on these features in -/// `examples/ml_tuner_demo.rs` — aprender is a dev-dependency so the SIMD foundation -/// stays free of the ML layer (no compute→core layer inversion). +/// `crates/aprender-core/tests/tuner_training_data_quality.rs` — compute has no +/// dependency on aprender-core, so the SIMD foundation stays free of the ML layer. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct KernelClassifier { /// Kernel accuracy on validation (for confidence) diff --git a/crates/aprender-compute/src/tuner/models/throughput.rs b/crates/aprender-compute/src/tuner/models/throughput.rs index 373f7bdde9..9c4ff3f23a 100644 --- a/crates/aprender-compute/src/tuner/models/throughput.rs +++ b/crates/aprender-compute/src/tuner/models/throughput.rs @@ -8,10 +8,10 @@ use super::ThroughputPrediction; /// Simple linear regression model for throughput prediction. /// -/// Uses closed-form solution: w = (X^T X)^-1 X^T y. The `ml-tuner` showcase +/// Uses closed-form solution: w = (X^T X)^-1 X^T y. The RandomForest showcase /// (SHOWCASE-BRICK-001) feeds these features into `aprender::RandomForestRegressor` -/// from `examples/ml_tuner_demo.rs` (aprender is a dev-dependency — the SIMD foundation -/// must not depend on the ML layer). +/// from `crates/aprender-core/tests/tuner_training_data_quality.rs` (compute has no +/// aprender-core dependency — the SIMD foundation must not depend on the ML layer). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThroughputRegressor { /// Model weights (one per feature + bias) diff --git a/crates/aprender-compute/src/tuner/tests/coverage/gap_coverage.rs b/crates/aprender-compute/src/tuner/tests/coverage/gap_coverage.rs index 356ba8d255..9c4848100d 100644 --- a/crates/aprender-compute/src/tuner/tests/coverage/gap_coverage.rs +++ b/crates/aprender-compute/src/tuner/tests/coverage/gap_coverage.rs @@ -90,13 +90,13 @@ mod calibrate_tests { } // ============================================================================ -// 2-3. ml-tuner RandomForest showcase (RELOCATED — SHOWCASE-BRICK-001) +// 2-3. RandomForest showcase (RELOCATED — SHOWCASE-BRICK-001) // ============================================================================ // The `ThroughputRegressor::with_random_forest` / `train_random_forest` and // `KernelClassifier::train` wrappers were removed: the SIMD compute lib must not // depend on the ML layer (aprender). The aprender RandomForest showcase now lives in -// examples/ml_tuner_demo.rs + tests/tuner_falsification/training_data_quality.rs, -// which consume aprender as a dev-dependency. No lib coverage target remains here. +// crates/aprender-core/tests/tuner_training_data_quality.rs (FLOW-010, #5803), +// which tests it from the aprender-core side. No lib coverage target remains here. // ============================================================================ // 4. crc32_table (helpers.rs, line 10) — const fn coverage diff --git a/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs b/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs index 77e2f9ec82..759cab2cd5 100644 --- a/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs +++ b/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs @@ -1,101 +1,11 @@ //! F041-F060: Training Data Quality (20 points) //! -//! The `ml-tuner` showcase (SHOWCASE-BRICK-001) feeds trueno `TunerFeatures` into -//! `aprender::RandomForestRegressor`/`Classifier`. aprender is a dev-dependency of -//! aprender-compute (the SIMD foundation never depends on the ML layer), so these -//! falsification tests drive the aprender RandomForest pipeline directly, mirroring the -//! `<10 samples → InsufficientData` guard the old lib wrapper enforced. +//! The aprender RandomForest half (F041-F044, F046-F048) lives in +//! `crates/aprender-core/tests/tuner_training_data_quality.rs`: the SIMD foundation +//! takes no dependency, not even a dev one, on the ML layer (FLOW-010, #5803). use trueno::tuner::{ThroughputRegressor, TunerFeatures}; -#[allow(unused_imports)] -use trueno::tuner::{KernelClassifier, QuantType}; - -/// Train an aprender RandomForest regressor on trueno features, with the showcase's -/// `<10 samples` guard. Returns `Err` on insufficient data or matrix/fit failure. -#[cfg(feature = "ml-tuner")] -fn fit_throughput_rf( - rows: &[(TunerFeatures, f32)], - n_trees: usize, -) -> Result { - use aprender::primitives::{Matrix, Vector}; - use aprender::tree::RandomForestRegressor; - if rows.len() < 10 { - return Err(format!("insufficient data: {} < 10", rows.len())); - } - let dim = TunerFeatures::DIM; - let n = rows.len(); - let mut x = Vec::with_capacity(n * dim); - let mut y = Vec::with_capacity(n); - for (f, t) in rows { - x.extend(f.to_vector()); - y.push(*t); - } - let x_mat = Matrix::from_vec(n, dim, x).map_err(|e| e.to_string())?; - let mut rf = RandomForestRegressor::new(n_trees); - rf.fit(&x_mat, &Vector::from_vec(y)).map_err(|e| e.to_string())?; - Ok(rf) -} - -/// F041: Empty training data should error -#[cfg(feature = "ml-tuner")] -#[test] -fn f041_empty_training_errors() { - let empty_data: Vec<(TunerFeatures, f32)> = vec![]; - let result = fit_throughput_rf(&empty_data, 10); - assert!(result.is_err(), "F041 FALSIFIED: empty training data should error"); -} - -/// F042: Single sample training should error gracefully (below the 10-sample floor) -#[cfg(feature = "ml-tuner")] -#[test] -fn f042_single_sample_graceful() { - let features = TunerFeatures::builder().model_params_b(1.5).build(); - let data = vec![(features, 100.0)]; - // Should error (insufficient), not panic - let _ = fit_throughput_rf(&data, 10); -} - -/// F043: Training with NaN labels should not produce NaN predictions -#[cfg(feature = "ml-tuner")] -#[test] -fn f043_nan_labels_error() { - use aprender::primitives::Matrix; - let mut data: Vec<(TunerFeatures, f32)> = (0..10) - .map(|i| (TunerFeatures::builder().batch_size(1 + (i % 8) as u32).build(), 100.0)) - .collect(); - data[0].1 = f32::NAN; - if let Ok(rf) = fit_throughput_rf(&data, 10) { - let fx = Matrix::from_vec(1, TunerFeatures::DIM, data[1].0.to_vector().to_vec()).unwrap(); - let pred = rf.predict(&fx); - assert!( - pred.as_slice()[0].is_finite() || pred.as_slice()[0].is_nan(), - "F043: prediction must be a well-defined float" - ); - } -} - -/// F044: Training with negative labels should be handled without panic -#[cfg(feature = "ml-tuner")] -#[test] -fn f044_negative_labels_handled() { - let data: Vec<(TunerFeatures, f32)> = (0..12) - .map(|i| { - let f = TunerFeatures::builder().batch_size(1 + (i % 8) as u32).build(); - (f, if i % 2 == 0 { -100.0 } else { 100.0 }) - }) - .collect(); - // Must not panic; result may be Ok or Err. - let _ = fit_throughput_rf(&data, 10); -} - -// Stub tests for non-ml-tuner builds -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f041_f044_ml_tuner_disabled() { - // Pass - these tests require ml-tuner feature -} - /// F045: Heuristic model should work without training #[test] fn f045_heuristic_no_training() { @@ -106,89 +16,6 @@ fn f045_heuristic_no_training() { assert!(pred.predicted_tps > 0.0, "F045 FALSIFIED: heuristic prediction failed"); } -/// F046: Training improves over heuristic (or doesn't regress) -#[cfg(feature = "ml-tuner")] -#[test] -fn f046_training_improves() { - // Generate training data that matches heuristic pattern - let training_data: Vec<(TunerFeatures, f32)> = (0..50) - .map(|i| { - let batch = 1 + (i % 8) as u32; - let features = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .gpu_mem_bw_gbs(1000.0) - .build(); - // Throughput scales with batch size - let throughput = 100.0 + (batch as f32) * 50.0; - (features, throughput) - }) - .collect(); - - let result = fit_throughput_rf(&training_data, 50); - assert!(result.is_ok(), "F046 FALSIFIED: training failed: {:?}", result.err()); -} - -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f046_ml_tuner_disabled() { - // Pass -} - -/// F047: Large training set should not OOM -#[cfg(feature = "ml-tuner")] -#[test] -fn f047_large_training_no_oom() { - let training_data: Vec<(TunerFeatures, f32)> = (0..1000) - .map(|i| { - let features = TunerFeatures::builder() - .model_params_b((i % 10) as f32 * 0.5 + 0.5) - .batch_size((i % 8 + 1) as u32) - .build(); - (features, 100.0 + (i as f32)) - }) - .collect(); - - let result = fit_throughput_rf(&training_data, 10); - assert!(result.is_ok(), "F047 FALSIFIED: large training failed"); -} - -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f047_ml_tuner_disabled() { - // Pass -} - -/// F048: Classifier training should work (aprender RandomForestClassifier on trueno features) -#[cfg(feature = "ml-tuner")] -#[test] -fn f048_classifier_training() { - use aprender::primitives::Matrix; - use aprender::tree::RandomForestClassifier; - - let dim = TunerFeatures::DIM; - let n = 50usize; - let mut x = Vec::with_capacity(n * dim); - let mut y: Vec = Vec::with_capacity(n); - for i in 0..n { - let batch = 1 + (i % 8) as u32; - let f = TunerFeatures::builder().model_params_b(1.5).batch_size(batch).build(); - x.extend(f.to_vector()); - // Label: BatchedQ4K (3) for M>=4, VectorizedQ4K (2) otherwise - y.push(if batch >= 4 { 3 } else { 2 }); - } - let x_mat = Matrix::from_vec(n, dim, x).expect("feature matrix"); - let mut classifier = RandomForestClassifier::new(10); - let result = classifier.fit(&x_mat, &y); - assert!(result.is_ok(), "F048 FALSIFIED: classifier training failed: {:?}", result.err()); -} - -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f048_ml_tuner_disabled() { - // Pass -} - /// F049: Training data variance check #[test] fn f049_training_data_variance() { diff --git a/crates/aprender-contracts-key/Cargo.toml b/crates/aprender-contracts-key/Cargo.toml new file mode 100644 index 0000000000..c5a827ff08 --- /dev/null +++ b/crates/aprender-contracts-key/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "aprender-contracts-key" +description = "The one CONTRACT_* env-var key builder shared by build.rs scripts and aprender-contracts (no dependencies, so build-depending on it does not mark the contract engine affected)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords = ["verification", "contracts"] +categories = ["development-tools::build-utils"] + +[lints] +workspace = true diff --git a/crates/aprender-contracts-key/README.md b/crates/aprender-contracts-key/README.md new file mode 100644 index 0000000000..4be79576f6 --- /dev/null +++ b/crates/aprender-contracts-key/README.md @@ -0,0 +1,14 @@ +# aprender-contracts-key + +The one `CONTRACT_*` env-var key builder shared by `build.rs` scripts and `aprender-contracts`. + +Part of the [Aprender](https://github.com/paiml/aprender) monorepo (`paiml/aprender`). It has no dependencies on purpose: a crate that build-depends on it does not mark the contract engine affected. + +```rust +let key = aprender_contracts_key::env_key("my-contract", "eq.1"); +assert_eq!(key, "CONTRACT_MY_CONTRACT_EQ_1"); +``` + +## License + +MIT diff --git a/crates/aprender-contracts-key/src/lib.rs b/crates/aprender-contracts-key/src/lib.rs new file mode 100644 index 0000000000..cff26360f9 --- /dev/null +++ b/crates/aprender-contracts-key/src/lib.rs @@ -0,0 +1,15 @@ +//! The `CONTRACT_*` env-var key builder (#4369, #5909). Dependency-free on purpose. + +/// The env var key `#[contract(contract, equation = equation)]` reads. +/// +/// A producer (a consuming crate's `build.rs`) must emit its +/// `cargo:rustc-env` vars under exactly this key, plus `_PRE_COUNT`, +/// `_PRE_`, `_POST_COUNT` and `_POST_`. Call this instead of +/// re-deriving the format: a hand-rolled copy that drifts makes every +/// condition land under a name the macro never reads (#2699 §4). It is +/// tested equal to the macro's own `contract_env_key!`. +pub fn env_key(contract: &str, equation: &str) -> String { + let contract_part = contract.to_uppercase().replace(['-', '.'], "_"); + let equation_part = equation.to_uppercase().replace(['-', '.'], "_"); + format!("CONTRACT_{contract_part}_{equation_part}") +} diff --git a/crates/aprender-contracts/Cargo.toml b/crates/aprender-contracts/Cargo.toml index 7be48d9776..cbcc4d9954 100644 --- a/crates/aprender-contracts/Cargo.toml +++ b/crates/aprender-contracts/Cargo.toml @@ -36,8 +36,10 @@ serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } thiserror = { workspace = true } +aprender-contracts-key = { path = "../aprender-contracts-key", version = "0.71.3" } [build-dependencies] +aprender-contracts-key = { path = "../aprender-contracts-key", version = "0.71.3" } serde = { version = "1", features = ["derive"] } serde_yaml = "0.9" diff --git a/crates/aprender-contracts/build.rs b/crates/aprender-contracts/build.rs index 69a8e70a05..a444cf56f4 100644 --- a/crates/aprender-contracts/build.rs +++ b/crates/aprender-contracts/build.rs @@ -3,8 +3,8 @@ use serde::Deserialize; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -// The one key builder, shared with `build_helper::env_key` (#4369). -include!("src/env_key.rs"); +// The one key builder lives in aprender-contracts-key (#4369, #5909). +use aprender_contracts_key::env_key; #[derive(Deserialize)] struct BindingFile { diff --git a/crates/aprender-contracts/src/env_key.rs b/crates/aprender-contracts/src/env_key.rs index bc38586f53..877a0504db 100644 --- a/crates/aprender-contracts/src/env_key.rs +++ b/crates/aprender-contracts/src/env_key.rs @@ -1,16 +1,3 @@ -// `include!`d by `build_helper.rs` (which carries the equality test against the -// macro) and by this crate's own build.rs, which cannot build-depend on the -// crate it builds (#4369). -/// The env var key `#[contract(contract, equation = equation)]` reads. -/// -/// A producer (a consuming crate's `build.rs`) must emit its -/// `cargo:rustc-env` vars under exactly this key, plus `_PRE_COUNT`, -/// `_PRE_`, `_POST_COUNT` and `_POST_`. Call this instead of -/// re-deriving the format: a hand-rolled copy that drifts makes every -/// condition land under a name the macro never reads (#2699 §4). It is -/// tested equal to the macro's own `contract_env_key!`. -pub fn env_key(contract: &str, equation: &str) -> String { - let contract_part = contract.to_uppercase().replace(['-', '.'], "_"); - let equation_part = equation.to_uppercase().replace(['-', '.'], "_"); - format!("CONTRACT_{contract_part}_{equation_part}") -} +// `include!`d by `build_helper.rs`. The +// definition lives in the dependency-free `aprender-contracts-key` (#4369, #5909). +pub use aprender_contracts_key::env_key; diff --git a/crates/aprender-core/Cargo.toml b/crates/aprender-core/Cargo.toml index 2c6a34777c..9fad293538 100644 --- a/crates/aprender-core/Cargo.toml +++ b/crates/aprender-core/Cargo.toml @@ -213,7 +213,7 @@ provable-contracts-macros = { workspace = true } # realizar = { version = "0.6", default-features = false, optional = true } # BLOCKED [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) serde = { version = "1", features = ["derive"] } serde-saphyr = { workspace = true } diff --git a/crates/aprender-core/build.rs b/crates/aprender-core/build.rs index 997babbe67..c0e29dc397 100644 --- a/crates/aprender-core/build.rs +++ b/crates/aprender-core/build.rs @@ -50,7 +50,7 @@ struct Binding { /// /// "softmax-kernel-v1.yaml" + "softmax" -> "CONTRACT_SOFTMAX_KERNEL_V1_SOFTMAX" fn binding_env_var_name(contract: &str, equation: &str) -> String { - provable_contracts::build_helper::env_key( + aprender_contracts_key::env_key( contract.trim_end_matches(".yaml").trim_end_matches(".yml"), equation, ) diff --git a/crates/aprender-core/src/tree/random_forest.rs b/crates/aprender-core/src/tree/random_forest.rs index 127b1ce0d6..7c3e5bb670 100644 --- a/crates/aprender-core/src/tree/random_forest.rs +++ b/crates/aprender-core/src/tree/random_forest.rs @@ -85,6 +85,10 @@ impl RandomForestRegressor { if n_samples == 0 { return Err("Cannot fit with zero samples".into()); } + // A NaN or infinite label would be fitted silently and make every prediction NaN (#6125). + if let Some(i) = y.as_slice().iter().position(|v| !v.is_finite()) { + return Err(format!("non-finite label at row {i}").into()); + } self.trees = Vec::with_capacity(self.n_estimators); self.oob_indices = Vec::with_capacity(self.n_estimators); diff --git a/crates/aprender-core/src/tree/random_forest_tests_regressor.rs b/crates/aprender-core/src/tree/random_forest_tests_regressor.rs index 7e5104d991..ce7baecb25 100644 --- a/crates/aprender-core/src/tree/random_forest_tests_regressor.rs +++ b/crates/aprender-core/src/tree/random_forest_tests_regressor.rs @@ -425,3 +425,23 @@ let sum: f32 = importances.iter().sum(); assert!((sum - 1.0).abs() < 1e-5, "importances must sum to 1: {sum}"); } + + #[test] + fn test_regressor_fit_refuses_non_finite_labels() { + // #6125: fit used to accept NaN/inf labels and then predict NaN for every row. + for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + let (x, y) = regression_data(); + let mut labels = y.as_slice().to_vec(); + labels[2] = bad; + let mut rf = RandomForestRegressor::new(3).with_random_state(1); + let err = rf + .fit(&x, &Vector::from_vec(labels)) + .expect_err("a non-finite label must be refused"); + assert_eq!(err.to_string(), "non-finite label at row 2", "label {bad}"); + assert!(rf.trees.is_empty(), "a refused fit must not leave trees behind"); + } + let (x, y) = regression_data(); + let mut rf = RandomForestRegressor::new(3).with_random_state(1); + rf.fit(&x, &y).expect("finite labels still fit"); + assert!(rf.predict(&x).as_slice().iter().all(|p| p.is_finite())); + } diff --git a/crates/aprender-core/tests/tuner_training_data_quality.rs b/crates/aprender-core/tests/tuner_training_data_quality.rs new file mode 100644 index 0000000000..2009195d32 --- /dev/null +++ b/crates/aprender-core/tests/tuner_training_data_quality.rs @@ -0,0 +1,182 @@ +#![allow(clippy::disallowed_methods, clippy::float_cmp)] +//! F041-F060 (aprender RandomForest half): Training Data Quality, ML-tuner showcase. +//! +//! SHOWCASE-BRICK-001 feeds trueno `TunerFeatures` into `aprender::RandomForestRegressor` +//! and `RandomForestClassifier`. These tests lived in aprender-compute behind a +//! `ml-tuner` feature nothing enabled, driven by a dev-dependency of compute on core. +//! That dev edge tied compute (the base of the stack) to core, so a touch of any base +//! crate selected most of the workspace (FLOW-010, #5803). Core depends on compute, so a +//! change to compute still selects these tests here. The tests that need no aprender stay +//! in `crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs`. + +use trueno::tuner::TunerFeatures; + +/// Train an aprender RandomForest regressor on trueno features, with the showcase's +/// `<10 samples` guard. Returns `Err` on insufficient data or matrix/fit failure. +fn fit_throughput_rf( + rows: &[(TunerFeatures, f32)], + n_trees: usize, +) -> Result { + use aprender::primitives::{Matrix, Vector}; + use aprender::tree::RandomForestRegressor; + if rows.len() < 10 { + return Err(format!("insufficient data: {} < 10", rows.len())); + } + let dim = TunerFeatures::DIM; + let n = rows.len(); + let mut x = Vec::with_capacity(n * dim); + let mut y = Vec::with_capacity(n); + for (f, t) in rows { + x.extend(f.to_vector()); + y.push(*t); + } + let x_mat = Matrix::from_vec(n, dim, x).map_err(|e| e.to_string())?; + let mut rf = RandomForestRegressor::new(n_trees); + rf.fit(&x_mat, &Vector::from_vec(y)) + .map_err(|e| e.to_string())?; + Ok(rf) +} + +/// F041: Empty training data should error +#[test] +fn f041_empty_training_errors() { + let empty_data: Vec<(TunerFeatures, f32)> = vec![]; + let result = fit_throughput_rf(&empty_data, 10); + assert!( + result.is_err(), + "F041 FALSIFIED: empty training data should error" + ); +} + +/// F042: Single sample training should error gracefully (below the 10-sample floor) +#[test] +fn f042_single_sample_graceful() { + let features = TunerFeatures::builder().model_params_b(1.5).build(); + let data = vec![(features, 100.0)]; + let err = fit_throughput_rf(&data, 10) + .err() + .expect("F042: a single sample must error"); + assert!( + err.contains("insufficient data"), + "F042: wrong error: {err}" + ); +} + +/// F043: Training with NaN labels must error (never fit and predict NaN) +#[test] +fn f043_nan_labels_error() { + let mut data: Vec<(TunerFeatures, f32)> = (0..10) + .map(|i| { + ( + TunerFeatures::builder() + .batch_size(1 + (i % 8) as u32) + .build(), + 100.0, + ) + }) + .collect(); + data[0].1 = f32::NAN; + let err = fit_throughput_rf(&data, 10) + .err() + .expect("F043: NaN labels must be refused, not fitted"); + assert!( + err.contains("non-finite label at row 0"), + "F043: wrong error: {err}" + ); +} + +/// F044: Training with negative labels should be handled without panic +#[test] +fn f044_negative_labels_handled() { + let data: Vec<(TunerFeatures, f32)> = (0..12) + .map(|i| { + let f = TunerFeatures::builder() + .batch_size(1 + (i % 8) as u32) + .build(); + (f, if i % 2 == 0 { -100.0 } else { 100.0 }) + }) + .collect(); + use aprender::primitives::Matrix; + let rf = fit_throughput_rf(&data, 10).expect("F044: negative finite labels must fit"); + for (feat, _) in &data { + let fx = Matrix::from_vec(1, TunerFeatures::DIM, feat.to_vector().to_vec()).unwrap(); + let p = rf.predict(&fx).as_slice()[0]; + assert!( + p.is_finite() && (-100.0..=100.0).contains(&p), + "F044: prediction {p} outside the label range" + ); + } +} + +/// F046: Training improves over heuristic (or doesn't regress) +#[test] +fn f046_training_improves() { + // Generate training data that matches heuristic pattern + let training_data: Vec<(TunerFeatures, f32)> = (0..50) + .map(|i| { + let batch = 1 + (i % 8) as u32; + let features = TunerFeatures::builder() + .model_params_b(1.5) + .batch_size(batch) + .gpu_mem_bw_gbs(1000.0) + .build(); + // Throughput scales with batch size + let throughput = 100.0 + (batch as f32) * 50.0; + (features, throughput) + }) + .collect(); + + let result = fit_throughput_rf(&training_data, 50); + assert!( + result.is_ok(), + "F046 FALSIFIED: training failed: {:?}", + result.err() + ); +} + +/// F047: Large training set should not OOM +#[test] +fn f047_large_training_no_oom() { + let training_data: Vec<(TunerFeatures, f32)> = (0..1000) + .map(|i| { + let features = TunerFeatures::builder() + .model_params_b((i % 10) as f32 * 0.5 + 0.5) + .batch_size((i % 8 + 1) as u32) + .build(); + (features, 100.0 + (i as f32)) + }) + .collect(); + + let result = fit_throughput_rf(&training_data, 10); + assert!(result.is_ok(), "F047 FALSIFIED: large training failed"); +} + +/// F048: Classifier training should work (aprender RandomForestClassifier on trueno features) +#[test] +fn f048_classifier_training() { + use aprender::primitives::Matrix; + use aprender::tree::RandomForestClassifier; + + let dim = TunerFeatures::DIM; + let n = 50usize; + let mut x = Vec::with_capacity(n * dim); + let mut y: Vec = Vec::with_capacity(n); + for i in 0..n { + let batch = 1 + (i % 8) as u32; + let f = TunerFeatures::builder() + .model_params_b(1.5) + .batch_size(batch) + .build(); + x.extend(f.to_vector()); + // Label: BatchedQ4K (3) for M>=4, VectorizedQ4K (2) otherwise + y.push(if batch >= 4 { 3 } else { 2 }); + } + let x_mat = Matrix::from_vec(n, dim, x).expect("feature matrix"); + let mut classifier = RandomForestClassifier::new(10); + let result = classifier.fit(&x_mat, &y); + assert!( + result.is_ok(), + "F048 FALSIFIED: classifier training failed: {:?}", + result.err() + ); +} diff --git a/crates/aprender-db/Cargo.toml b/crates/aprender-db/Cargo.toml index f60b03b5c6..a2cab3378c 100644 --- a/crates/aprender-db/Cargo.toml +++ b/crates/aprender-db/Cargo.toml @@ -66,7 +66,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) aprender-build-sha = { workspace = true } # #4219: APR_GIT_SHA for --version serde = { version = "1", features = ["derive"] } serde-saphyr = { workspace = true } diff --git a/crates/aprender-db/build.rs b/crates/aprender-db/build.rs index dcc8913930..ffb4d4d655 100644 --- a/crates/aprender-db/build.rs +++ b/crates/aprender-db/build.rs @@ -49,10 +49,8 @@ fn main() { let total = bindings.bindings.len() as u32; for b in &bindings.bindings { - let var = provable_contracts::build_helper::env_key( - b.contract.trim_end_matches(".yaml"), - &b.equation, - ); + let var = + aprender_contracts_key::env_key(b.contract.trim_end_matches(".yaml"), &b.equation); println!("cargo:rustc-env={var}={}", b.status); if b.status == "implemented" { implemented += 1; diff --git a/crates/aprender-orchestrate/Cargo.toml b/crates/aprender-orchestrate/Cargo.toml index 0d2a522d2d..99b8774af5 100644 --- a/crates/aprender-orchestrate/Cargo.toml +++ b/crates/aprender-orchestrate/Cargo.toml @@ -317,7 +317,7 @@ sovereign-stack = [ ] [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) aprender-build-sha = { workspace = true } # #4219: APR_GIT_SHA for --version serde = { version = "1", features = ["derive"] } serde-saphyr = { workspace = true } diff --git a/crates/aprender-orchestrate/build.rs b/crates/aprender-orchestrate/build.rs index 8f14315f0c..2fba89145c 100644 --- a/crates/aprender-orchestrate/build.rs +++ b/crates/aprender-orchestrate/build.rs @@ -80,10 +80,8 @@ fn emit_bindings(bindings: &BindingFile) { let total = bindings.bindings.len() as u32; for b in &bindings.bindings { - let var = provable_contracts::build_helper::env_key( - b.contract.trim_end_matches(".yaml"), - &b.equation, - ); + let var = + aprender_contracts_key::env_key(b.contract.trim_end_matches(".yaml"), &b.equation); println!("cargo:rustc-env={var}={}", b.status); if b.status == "implemented" { implemented += 1; @@ -117,7 +115,7 @@ fn emit_contract_assertions(cdir: &Path) { continue; }; for (n, eq) in &y.equations { - let k = provable_contracts::build_helper::env_key(&s, n); + let k = aprender_contracts_key::env_key(&s, n); tp += emit_conditions(&k, "PRE", &eq.preconditions); tq += emit_conditions(&k, "POST", &eq.postconditions); } diff --git a/crates/aprender-present-cli/Cargo.toml b/crates/aprender-present-cli/Cargo.toml index f66f0f45ab..9249c0e6f6 100644 --- a/crates/aprender-present-cli/Cargo.toml +++ b/crates/aprender-present-cli/Cargo.toml @@ -22,7 +22,7 @@ tiny_http = "0.12" tungstenite = { version = "0.26", optional = true } [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) aprender-build-sha = { workspace = true } # #4219: APR_GIT_SHA for --version serde = { version = "1", features = ["derive"] } serde-saphyr = { workspace = true } diff --git a/crates/aprender-present-cli/build.rs b/crates/aprender-present-cli/build.rs index cf8320e713..b5f8c3f8ea 100644 --- a/crates/aprender-present-cli/build.rs +++ b/crates/aprender-present-cli/build.rs @@ -49,10 +49,8 @@ fn main() { let total = bindings.bindings.len() as u32; for b in &bindings.bindings { - let var = provable_contracts::build_helper::env_key( - b.contract.trim_end_matches(".yaml"), - &b.equation, - ); + let var = + aprender_contracts_key::env_key(b.contract.trim_end_matches(".yaml"), &b.equation); println!("cargo:rustc-env={var}={}", b.status); if b.status == "implemented" { implemented += 1; diff --git a/crates/aprender-present-core/Cargo.toml b/crates/aprender-present-core/Cargo.toml index bcd37afbe3..f696474475 100644 --- a/crates/aprender-present-core/Cargo.toml +++ b/crates/aprender-present-core/Cargo.toml @@ -23,7 +23,7 @@ serde_json = { workspace = true } provable-contracts-macros = { workspace = true } [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) serde = { workspace = true } serde-saphyr = { workspace = true } diff --git a/crates/aprender-present-core/build.rs b/crates/aprender-present-core/build.rs index e86c6773da..467ba3996e 100644 --- a/crates/aprender-present-core/build.rs +++ b/crates/aprender-present-core/build.rs @@ -84,7 +84,7 @@ fn emit_contract_file_pre_post(path: &PathBuf, total_pre: &mut usize, total_post return; }; for (eq_name, eq) in &parsed.equations { - let key = provable_contracts::build_helper::env_key(&stem, eq_name); + let key = aprender_contracts_key::env_key(&stem, eq_name); emit_pre_post_lists(&key, eq, total_pre, total_post); } } @@ -178,7 +178,7 @@ fn emit_binding_counts_and_vars(bf: &BindingFile) { } fn env_var_name(contract: &str, equation: &str) -> String { - provable_contracts::build_helper::env_key( + aprender_contracts_key::env_key( contract.trim_end_matches(".yaml").trim_end_matches(".yml"), equation, ) diff --git a/crates/aprender-rag/Cargo.toml b/crates/aprender-rag/Cargo.toml index f2f4ff3900..b1838638e8 100644 --- a/crates/aprender-rag/Cargo.toml +++ b/crates/aprender-rag/Cargo.toml @@ -72,7 +72,7 @@ sha2 = { version = "0.10", optional = true } rand = { version = "0.9", optional = true } [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) serde = { version = "1", features = ["derive"] } serde-saphyr = { workspace = true } diff --git a/crates/aprender-rag/build.rs b/crates/aprender-rag/build.rs index cbf56d77fe..df297328bc 100644 --- a/crates/aprender-rag/build.rs +++ b/crates/aprender-rag/build.rs @@ -46,10 +46,8 @@ fn main() { let total = bindings.bindings.len() as u32; for b in &bindings.bindings { - let var = provable_contracts::build_helper::env_key( - b.contract.trim_end_matches(".yaml"), - &b.equation, - ); + let var = + aprender_contracts_key::env_key(b.contract.trim_end_matches(".yaml"), &b.equation); println!("cargo:rustc-env={var}={}", b.status); if b.status == "implemented" { implemented += 1; diff --git a/crates/aprender-simulate/Cargo.toml b/crates/aprender-simulate/Cargo.toml index e5ff4de6bf..858d269d45 100644 --- a/crates/aprender-simulate/Cargo.toml +++ b/crates/aprender-simulate/Cargo.toml @@ -94,7 +94,7 @@ provable-contracts-macros = { workspace = true } # Code gated behind #[cfg(feature = "z3-proofs")] [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) aprender-build-sha = { workspace = true } # #4219: APR_GIT_SHA for --version serde = { version = "1.0", features = ["derive"] } serde-saphyr = { workspace = true } # #5208: build.rs reads contract YAML through saphyr, not unsafe-libyaml diff --git a/crates/aprender-simulate/build.rs b/crates/aprender-simulate/build.rs index cb4d91ca06..f75b3d022f 100644 --- a/crates/aprender-simulate/build.rs +++ b/crates/aprender-simulate/build.rs @@ -120,7 +120,7 @@ fn emit_contract_file(path: &Path, total_pre: &mut usize, total_post: &mut usize return; }; for (eq_name, equation) in &contract.equations { - let key = provable_contracts::build_helper::env_key(stem, eq_name); + let key = aprender_contracts_key::env_key(stem, eq_name); emit_pre_post(&key, equation, total_pre, total_post); } } @@ -185,10 +185,8 @@ fn enforce_provable_binding() { fn emit_binding_vars(bf: &BindingFile) -> Vec { let mut gaps = Vec::new(); for b in &bf.bindings { - let var = provable_contracts::build_helper::env_key( - b.contract.trim_end_matches(".yaml"), - &b.equation, - ); + let var = + aprender_contracts_key::env_key(b.contract.trim_end_matches(".yaml"), &b.equation); println!("cargo:rustc-env={var}={}", b.status); if b.status != "implemented" { gaps.push(var); diff --git a/crates/aprender-simulate/src/scenarios/course_flow.rs b/crates/aprender-simulate/src/scenarios/course_flow.rs index cb59dd42e2..1cbb486724 100644 --- a/crates/aprender-simulate/src/scenarios/course_flow.rs +++ b/crates/aprender-simulate/src/scenarios/course_flow.rs @@ -40,6 +40,16 @@ pub struct CourseFlowConfig { pub verify_min: f64, /// Count a finished course as WIP until the daily batch releases it (flow model B6). pub batch_in_wip: bool, + /// Content Factory v2 (B5a): every master fans out to edX, Coursera and LinkedIn; `coursera_share` is ignored. + pub fanout: bool, + /// Rework probability after a build (B5b): a build repeats with this probability, so B' = B/(1-alpha). + pub alpha: f64, + /// Trunk-first GPU rule (B5c): while the running mean lead exceeds this many days the GPU serves + /// courses before Shorts. 0 keeps Shorts first always. + pub trunk_first_w_d: f64, + /// LinkedIn renders per master are `li_k + 4` on the GPU (B5d). + pub li_k: usize, + pub li_render_min: f64, } /// What one seeded run measured after the warm-up. @@ -52,6 +62,10 @@ pub struct CourseFlowResult { pub util: [f64; 6], /// Mean hours a built course waits for the daily batch (courses that take the batch path). pub mean_batch_wait_h: f64, + /// Mean build service hours per course, rework included: the sizing figure (flow model eq 2). + pub t_b_work_h: f64, + /// Mean hours from first build queueing to last build exit: the lead-time figure (eq 1). + pub t_b_elapsed_h: f64, } impl CourseFlowResult { @@ -142,6 +156,16 @@ struct Sim { in_system: usize, l_area: f64, last_t: f64, + left_wip: Vec, + left_launch: Vec, + li_left: Vec, + build_in: Vec, + build_work: Vec, + tb_work: f64, + tb_elapsed: f64, + tb_n: u64, + lead_sum: f64, + lead_n: u64, } impl Sim { @@ -157,7 +181,15 @@ impl Sim { fn enqueue(&mut self, s: usize, job: Job, mean: f64) { let d = self.erlang4(mean); - if job.short { + if s == BUILD && !job.short { + self.build_work[job.id] += d; + } + let first = if self.trunk_first(s) { + !job.short + } else { + job.short + }; + if first { self.st[s].hi.push_back((job, d)); } else { self.st[s].lo.push_back((job, d)); @@ -187,6 +219,11 @@ impl Sim { self.started_at.push(self.now); let cs = self.rng.gen_f64() < self.c.coursera_share; self.coursera.push(cs); + self.left_wip.push(0); + self.left_launch.push(0); + self.li_left.push(0); + self.build_in.push(0.0); + self.build_work.push(0.0); self.wip += 1; self.in_system += 1; let m = self.c.outline_min; @@ -201,11 +238,58 @@ impl Sim { ); } + /// B5c: on the GPU, courses go first while the running mean lead exceeds the threshold. + /// The mean runs from t = 0 over every finished course (warm-up included). It is cumulative, so + /// it moves slowly and can fall below the threshold again; a windowed mean is a follow-up. + fn trunk_first(&self, s: usize) -> bool { + trunk_first_rule(s, self.c.trunk_first_w_d, self.lead_sum, self.lead_n) + } + + /// A branch of master `id` is done with its WIP slot; the slot frees when every branch is. + fn wip_done(&mut self, id: usize) { + if !self.c.fanout { + return self.leave_wip(); + } + self.left_wip[id] -= 1; + if self.left_wip[id] == 0 { + self.leave_wip(); + } + } + + /// A branch of master `id` is live; the master launches when every branch is. + fn launch_done(&mut self, id: usize) { + if !self.c.fanout { + return self.launch(id); + } + self.left_launch[id] -= 1; + if self.left_launch[id] == 0 { + self.launch(id); + } + } + + /// B5a: after verify, one branch per platform: edX (no human step), Coursera (stage, then batch), + /// LinkedIn (k + 4 renders on the GPU). + fn fan_out(&mut self, j: Job) { + let c = self.c.clone(); + let nj = |step| Job { step, ..j }; + let renders = c.li_k + 4; + self.left_wip[j.id] = 3; + self.left_launch[j.id] = 3; + self.li_left[j.id] = renders; + self.at(15.0, &Ev::Delay(nj(6))); + self.enqueue(CHROME, nj(5), c.stage_min); + for _ in 0..renders { + self.enqueue(RENDER, nj(7), c.li_render_min); + } + } + fn launch(&mut self, id: usize) { self.in_system -= 1; if self.now >= self.warm { self.sojourn.push(self.now - self.started_at[id]); } + self.lead_sum += self.now - self.started_at[id]; + self.lead_n += 1; } fn leave_wip(&mut self) { @@ -220,21 +304,42 @@ impl Sim { let c = self.c.clone(); let nj = |step| Job { step, ..j }; match j.step { - 0 => self.enqueue(BUILD, nj(1), c.build_min), - 1 => self.enqueue(DESIGN, nj(2), c.design_min), + 0 => { + self.build_in[j.id] = self.now; + self.enqueue(BUILD, nj(1), c.build_min); + } + 1 if c.alpha > 0.0 && self.rng.gen_f64() < c.alpha => { + self.enqueue(BUILD, nj(1), c.build_min); + } + 1 => { + if self.now >= self.warm { + self.tb_work += self.build_work[j.id]; + self.tb_elapsed += self.now - self.build_in[j.id]; + self.tb_n += 1; + } + self.enqueue(DESIGN, nj(2), c.design_min); + } 2 => self.enqueue(RENDER, nj(3), c.render_min), 3 => self.enqueue(VERIFY, nj(4), c.verify_min), + 4 if c.fanout => self.fan_out(j), 4 if self.coursera[j.id] => self.enqueue(CHROME, nj(5), c.stage_min), 4 => self.at(15.0, &Ev::Delay(nj(6))), + 7 => { + self.li_left[j.id] -= 1; + if self.li_left[j.id] == 0 { + self.wip_done(j.id); + self.launch_done(j.id); + } + } 5 => { if !self.c.batch_in_wip { - self.leave_wip(); + self.wip_done(j.id); } self.batch_wait.push((j.id, self.now)); } _ => { - self.leave_wip(); - self.launch(j.id); + self.wip_done(j.id); + self.launch_done(j.id); } } } @@ -247,9 +352,9 @@ impl Sim { self.bw_n += 1; } if self.c.batch_in_wip { - self.leave_wip(); + self.wip_done(id); } - self.launch(id); + self.launch_done(id); } } @@ -308,6 +413,12 @@ impl Sim { } } +/// The trunk-first predicate: only the render stage, only with a positive threshold, only once a +/// course has finished, and only while the mean lead (days) is above the threshold. +fn trunk_first_rule(stage: usize, w_d: f64, lead_sum: f64, lead_n: u64) -> bool { + stage == RENDER && w_d > 0.0 && lead_n > 0 && lead_sum / lead_n as f64 / DAY > w_d +} + /// Run one seed for `days`, discarding the first `warmup_d`. #[must_use] pub fn run_course_flow( @@ -345,6 +456,16 @@ pub fn run_course_flow( in_system: 0, l_area: 0.0, last_t: 0.0, + left_wip: Vec::new(), + left_launch: Vec::new(), + li_left: Vec::new(), + build_in: Vec::new(), + build_work: Vec::new(), + tb_work: 0.0, + tb_elapsed: 0.0, + tb_n: 0, + lead_sum: 0.0, + lead_n: 0, }; s.at(0.0, &Ev::Start); s.at(0.0, &Ev::ShortTick); @@ -378,6 +499,8 @@ pub fn run_course_flow( l: s.l_area / span, util, mean_batch_wait_h: s.bw_sum / (s.bw_n.max(1) as f64) / 60.0, + t_b_work_h: s.tb_work / (s.tb_n.max(1) as f64) / 60.0, + t_b_elapsed_h: s.tb_elapsed / (s.tb_n.max(1) as f64) / 60.0, } } @@ -400,6 +523,11 @@ pub fn course_preset(name: &str) -> Option { outline_min: 90.0, verify_min: 15.0, batch_in_wip: false, + fanout: false, + alpha: 0.0, + trunk_first_w_d: 0.0, + li_k: 0, + li_render_min: 20.0, }; match name { "cal-today" => { @@ -411,6 +539,12 @@ pub fn course_preset(name: &str) -> Option { "nine-builders" => c.builders = 9, "coursera-24h" => c.stage_min = 1440.0, "render-300" => c.render_min = 300.0, + "factory-v2" => { + c.fanout = true; + c.alpha = 0.16; + c.trunk_first_w_d = 4.8; + c.li_k = 2; + } _ => return None, } Some(c) @@ -418,13 +552,14 @@ pub fn course_preset(name: &str) -> Option { /// Names of the FLOW-006 factory cases. #[must_use] -pub fn course_cases() -> [&'static str; 5] { +pub fn course_cases() -> [&'static str; 6] { [ "cal-today", "design", "nine-builders", "coursera-24h", "render-300", + "factory-v2", ] } @@ -439,6 +574,8 @@ pub fn run_course_reference(c: &CourseFlowConfig) -> CourseFlowResult { a.lead_days += r.lead_days / 5.0; a.l += r.l / 5.0; a.mean_batch_wait_h += r.mean_batch_wait_h / 5.0; + a.t_b_work_h += r.t_b_work_h / 5.0; + a.t_b_elapsed_h += r.t_b_elapsed_h / 5.0; for i in 0..6 { a.util[i] += r.util[i] / 5.0; } @@ -468,6 +605,21 @@ pub fn apply_course_inputs(c: &mut CourseFlowConfig, v: &serde_json::Value) -> R "starts_per_day" => c.starts_per_day = f, "shorts_per_day" => c.shorts_per_day = f, "batch_in_wip" => c.batch_in_wip = f != 0.0, + "fanout" => c.fanout = f != 0.0, + "alpha" => { + if !(0.0..1.0).contains(&f) { + return Err(format!("alpha must be in [0, 1), got {f}")); + } + c.alpha = f; + } + "trunk_first_w_d" => c.trunk_first_w_d = f, + "li_k" => { + if !(0.0..=64.0).contains(&f) { + return Err(format!("li_k must be in [0, 64], got {f}")); + } + c.li_k = f as usize; + } + "li_render_min" => c.li_render_min = f, _ => return Err(format!("unknown input: {k}")), } } @@ -491,10 +643,14 @@ pub fn course_sim_block( "chrome_lanes": c.chrome_lanes, "stage_min": c.stage_min, "coursera_share": c.coursera_share, "wip": c.wip, "starts_per_day": c.starts_per_day, "shorts_per_day": c.shorts_per_day, "batch_in_wip": c.batch_in_wip, + "fanout": c.fanout, "alpha": c.alpha, "trunk_first_w_d": c.trunk_first_w_d, + "li_k": c.li_k, "li_render_min": c.li_render_min, }, "predicted": { "courses_day": r.courses_per_day, "shorts_day": r.shorts_per_day, - "lead_days": r.lead_days, "L": r.l, "mean_batch_wait_h": r.mean_batch_wait_h, "busiest_util_pct": util * 100.0, + "lead_days": r.lead_days, "L": r.l, "mean_batch_wait_h": r.mean_batch_wait_h, + "t_b_work_h": r.t_b_work_h, "t_b_elapsed_h": r.t_b_elapsed_h, + "busiest_util_pct": util * 100.0, }, "binding_station": station, }) @@ -625,6 +781,130 @@ mod tests { assert!((a.courses_per_day - b.courses_per_day).abs() > 1e-6 || (a.l - b.l).abs() > 1e-6); } + fn v2() -> CourseFlowResult { + got("factory-v2") + } + + /// B5 falsifier: the Coursera stage at R = 3 carries every master, so busy = 5 x 360 / (2 x 1440) = 62.5%. + #[test] + fn coursera_stage_matches_the_law_within_two_points() { + let r = v2(); + assert!( + (r.util[CHROME] - (0.625 + 25.0 / 2880.0)).abs() <= 0.02, + "chrome busy {:.3} vs law 0.625 plus the Shorts stage", + r.util[CHROME] + ); + } + + /// B5 falsifier: at alpha = 0.16 the build stage runs at the law's B/(1-alpha) load (94.7% busy), + /// within two points. The bound is the law with a tolerance, not a bare 95% cap: the preset sits + /// 0.3 points under 95%, so a cap flips on any change to the random draw order. + #[test] + fn build_busy_at_alpha_016_matches_the_law() { + let r = v2(); + let law = 2520.0 / 0.84 * 5.0 / (11.0 * 1440.0); + assert!( + (r.util[BUILD] - law).abs() <= 0.02, + "build busy {:.3} vs law {law:.3}", + r.util[BUILD] + ); + assert!( + r.util[BUILD] > 0.85, + "rework not applied: {:.3}", + r.util[BUILD] + ); + } + + /// B5b: mean build work per course is B/(1-alpha) = 2520/0.84 min = 50 h. + #[test] + fn rework_loop_scales_build_work_by_one_over_one_minus_alpha() { + let r = v2(); + let want = 2520.0 / 0.84 / 60.0; + assert!( + (r.t_b_work_h - want).abs() <= want * 0.04, + "t_b_work_h {:.2} vs {want:.2}", + r.t_b_work_h + ); + } + + /// B5e: elapsed build time includes queueing, so it can only exceed the work. + #[test] + fn build_elapsed_is_not_the_build_work() { + let r = v2(); + assert!( + r.t_b_elapsed_h > r.t_b_work_h + 1e-6, + "{:.2} vs {:.2}", + r.t_b_elapsed_h, + r.t_b_work_h + ); + } + + /// B5a: fan-out to three platforms puts a Coursera batch wait on every master, so the wait is reported. + #[test] + fn every_master_fans_out_to_all_three_platforms() { + let mut c = course_preset("factory-v2").expect("case"); + c.fanout = false; + let split = run_course_reference(&c); + assert!( + v2().util[CHROME] > split.util[CHROME] + 0.2, + "fan-out did not raise chrome load" + ); + } + + /// B5d: LinkedIn adds k + 4 renders per master to the GPU. + #[test] + fn linkedin_renders_load_the_gpu() { + let mut c = course_preset("factory-v2").expect("case"); + c.li_k = 0; + let lo = run_course_reference(&c); + assert!( + v2().util[RENDER] > lo.util[RENDER] + 0.05, + "{:.3} vs {:.3}", + v2().util[RENDER], + lo.util[RENDER] + ); + } + + /// B5c: the trunk-first predicate, tested directly: the simulated outcome of the rule is within + /// seed noise (Shorts barely move), so the falsifier pins the rule and not a noisy delta. + #[test] + fn trunk_first_rule_changes_the_outcome() { + let d = DAY; + // mean lead 3 d: above a 2 d threshold, below a 4 d one + assert!(trunk_first_rule(RENDER, 2.0, 30.0 * d, 10)); + assert!(!trunk_first_rule(RENDER, 4.0, 30.0 * d, 10)); + // off at 0, off before any course finished, and only the render stage is reordered + assert!(!trunk_first_rule(RENDER, 0.0, 30.0 * d, 10)); + assert!(!trunk_first_rule(RENDER, 2.0, 30.0 * d, 0)); + assert!(!trunk_first_rule(BUILD, 2.0, 30.0 * d, 10)); + // the rule is wired into the run: some threshold engages it and changes the result + let run_w = |w: f64| { + let mut c = course_preset("factory-v2").expect("case"); + c.trunk_first_w_d = w; + run_course_reference(&c) + }; + let off = run_w(0.0); + let on = run_w(0.5); + assert!( + (on.lead_days - off.lead_days).abs() > 1e-6 + || (on.shorts_per_day - off.shorts_per_day).abs() > 1e-6 + ); + } + + /// alpha is a probability below 1: at 1 or more a build repeats forever. + #[test] + fn alpha_must_be_below_one() { + let mut c = course_preset("factory-v2").expect("case"); + for bad in [1.0, 1.5, -0.1] { + let e = apply_course_inputs(&mut c, &serde_json::json!({ "alpha": bad })); + assert!(e.is_err(), "alpha {bad} accepted"); + } + let e = apply_course_inputs(&mut c, &serde_json::json!({ "li_k": 1.0e30 })); + assert!(e.is_err(), "li_k 1e30 accepted"); + apply_course_inputs(&mut c, &serde_json::json!({ "alpha": 0.5 })).expect("0.5 is valid"); + assert!((c.alpha - 0.5).abs() < 1e-12); + } + #[test] fn unknown_case_is_none() { assert!(course_preset("nope").is_none()); diff --git a/crates/aprender-train/Cargo.toml b/crates/aprender-train/Cargo.toml index ac52f7d28d..dc0f9e5514 100644 --- a/crates/aprender-train/Cargo.toml +++ b/crates/aprender-train/Cargo.toml @@ -135,7 +135,7 @@ zip = { version = "9.0", default-features = false, features = ["deflate"] } # R alimentar = { workspace = true, optional = true } # Dataset loading with parquet/arrow support (gated behind parquet feature) [build-dependencies] -provable-contracts = { workspace = true } # #4369: build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { workspace = true } # #4369, #5909: the one CONTRACT_* key builder (no engine dependency) serde = { version = "1", features = ["derive"] } serde_yaml_ng = "0.10" diff --git a/crates/aprender-train/build.rs b/crates/aprender-train/build.rs index 9fc0b039c6..eeb44eba68 100644 --- a/crates/aprender-train/build.rs +++ b/crates/aprender-train/build.rs @@ -39,7 +39,7 @@ struct Binding { /// /// `"learning-rate-schedules-v1.yaml"` + `"cosine_warmup"` → `"CONTRACT_LEARNING_RATE_SCHEDULES_V1_COSINE_WARMUP"` fn env_var_name(contract: &str, equation: &str) -> String { - provable_contracts::build_helper::env_key( + aprender_contracts_key::env_key( contract.trim_end_matches(".yaml").trim_end_matches(".yml"), equation, ) @@ -260,7 +260,7 @@ fn emit_contract_file(path: &Path, total_pre: &mut usize, total_post: &mut usize return; }; for (eq_name, eq) in &parsed.equations { - let key = provable_contracts::build_helper::env_key(&stem, eq_name); + let key = aprender_contracts_key::env_key(&stem, eq_name); emit_pre_post(&key, eq, total_pre, total_post); } } diff --git a/crates/aprender-viz-ttop/Cargo.toml b/crates/aprender-viz-ttop/Cargo.toml index 077014a189..d96ef95f07 100644 --- a/crates/aprender-viz-ttop/Cargo.toml +++ b/crates/aprender-viz-ttop/Cargo.toml @@ -38,7 +38,7 @@ thiserror = "2.0" anyhow = "1.0" [build-dependencies] -provable-contracts = { path = "../aprender-contracts", package = "aprender-contracts" } # #4369 (excluded crate: no workspace deps): build_helper::env_key, the one CONTRACT_* key builder +aprender-contracts-key = { path = "../aprender-contracts-key" } # #4369, #5909 (excluded crate: no workspace deps): the one CONTRACT_* key builder serde = { version = "1.0", features = ["derive"] } serde-saphyr = { workspace = true } diff --git a/crates/aprender-viz-ttop/build.rs b/crates/aprender-viz-ttop/build.rs index d59e53abdb..f79fb9fca5 100644 --- a/crates/aprender-viz-ttop/build.rs +++ b/crates/aprender-viz-ttop/build.rs @@ -34,7 +34,7 @@ fn process_contract(path: &std::path::Path) -> (usize, usize) { let (mut pre, mut post) = (0, 0); for (name, eq) in &yaml.equations { - let key = provable_contracts::build_helper::env_key(&stem, name); + let key = aprender_contracts_key::env_key(&stem, name); if !eq.preconditions.is_empty() { println!("cargo:rustc-env={key}_PRE_COUNT={}", eq.preconditions.len()); pre += eq.preconditions.len(); diff --git a/crates/facades/Cargo.lock b/crates/facades/Cargo.lock index 4dfeab8f7c..c212413a9c 100644 --- a/crates/facades/Cargo.lock +++ b/crates/facades/Cargo.lock @@ -15,6 +15,7 @@ dependencies = [ name = "aprender-contracts" version = "0.71.3" dependencies = [ + "aprender-contracts-key", "aprender-contracts-macros", "blake3", "proc-macro2", @@ -27,6 +28,10 @@ dependencies = [ "thiserror", ] +[[package]] +name = "aprender-contracts-key" +version = "0.71.3" + [[package]] name = "aprender-contracts-macros" version = "0.71.3" diff --git a/docs/findings/w5032.jsonl b/docs/findings/w5032.jsonl new file mode 100644 index 0000000000..031a90b0fe --- /dev/null +++ b/docs/findings/w5032.jsonl @@ -0,0 +1 @@ +{"id":"FND-20261021-trunk-first-preset-weak","title":"factory-v2 preset trunk_first_w_d=4.8 d is a near no-op across seed windows","evidence":"B5a quorum round 2 lane 2: exact no-op in 8 of 9 seed windows; cumulative mean rarely exceeds 4.8 d","repro":"run_course_reference with trunk_first_w_d 4.8 vs 0 over 9 seed windows","suspected_epic":"unknown","severity":"P3","found_at_sha":"860900f622507cca752472dd1470d6104a8c5ddf"} diff --git a/docs/roadmaps/entries/PMAT-5502.yaml b/docs/roadmaps/entries/PMAT-5502.yaml new file mode 100644 index 0000000000..f898a773b7 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5502.yaml @@ -0,0 +1,19 @@ +- id: PMAT-5502 + github_issue: 5502 + item_type: task + title: Wire parity_receipt_denominator.sh verify mode into CI (#5502) + status: planned + priority: medium + assigned_to: null + created: 2026-10-18T10:18:08Z + updated: 2026-10-18T10:18:08Z + spec: null + acceptance_criteria: + - 'Slice of #5309 (debt ratchet)' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 3997 diff --git a/docs/roadmaps/entries/PMAT-5557.yaml b/docs/roadmaps/entries/PMAT-5557.yaml new file mode 100644 index 0000000000..c7ef7d0efc --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5557.yaml @@ -0,0 +1,18 @@ +- id: PMAT-5557 + github_issue: 5557 + item_type: task + title: 'feat(simulate): course_flow fan-out, rework loop, GPU priority (flow model B5)' + status: planned + priority: high + assigned_to: null + created: 2026-10-23T10:50:00Z + updated: 2026-10-23T10:50:00Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5923 diff --git a/docs/roadmaps/entries/PMAT-5803.yaml b/docs/roadmaps/entries/PMAT-5803.yaml new file mode 100644 index 0000000000..0c0feb61c1 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5803.yaml @@ -0,0 +1,19 @@ +- id: PMAT-5803 + github_issue: 5803 + item_type: task + title: 'FLOW-010 cut 1: drop aprender-compute dev-dep on aprender-core (tuner training-data tests move to aprender-core)' + status: planned + priority: high + assigned_to: null + created: 2026-10-23T10:40:00Z + updated: 2026-10-23T10:40:00Z + spec: null + acceptance_criteria: + - 'aprender-compute has no dev-dependency on aprender-core; the tuner training-data tests run from aprender-core and a guard fails if a moved test is missing, duplicated or ignored' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5923 diff --git a/docs/roadmaps/entries/PMAT-5909.yaml b/docs/roadmaps/entries/PMAT-5909.yaml new file mode 100644 index 0000000000..5edd99a0e1 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5909.yaml @@ -0,0 +1,19 @@ +- id: PMAT-5909 + github_issue: 5909 + item_type: task + title: 'Crate fan-out: aprender-contracts (24 commits/30d) dirties 47 of 85 crates; split build_helper out' + status: planned + priority: medium + assigned_to: null + created: 2026-10-22T12:00:00Z + updated: 2026-10-22T12:00:00Z + spec: null + acceptance_criteria: + - See issue #5909; registered so quorum-review.sh resolves the commit trailer (EMG epic PRs) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5510 diff --git a/docs/roadmaps/entries/PMAT-6125.yaml b/docs/roadmaps/entries/PMAT-6125.yaml new file mode 100644 index 0000000000..32f4b613d9 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-6125.yaml @@ -0,0 +1,18 @@ +- id: PMAT-6125 + github_issue: 6125 + item_type: task + title: 'fix(tree): RandomForestRegressor::fit accepts NaN labels and predicts NaN for every row' + status: planned + priority: high + assigned_to: null + created: 2026-10-23T10:50:00Z + updated: 2026-10-23T10:50:00Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5923 diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 31342f4a65..03185f1064 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -25321,6 +25321,43 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:code notes: null epic: 3998 +- id: PMAT-5502 + github_issue: 5502 + item_type: task + title: Wire parity_receipt_denominator.sh verify mode into CI (#5502) + status: planned + priority: medium + assigned_to: null + created: 2026-10-18T10:18:08Z + updated: 2026-10-18T10:18:08Z + spec: null + acceptance_criteria: + - 'Slice of #5309 (debt ratchet)' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 3997 +- id: PMAT-5557 + github_issue: 5557 + item_type: task + title: 'feat(simulate): course_flow fan-out, rework loop, GPU priority (flow model B5)' + status: planned + priority: high + assigned_to: null + created: 2026-10-23T10:50:00Z + updated: 2026-10-23T10:50:00Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5923 - id: PMAT-5737 github_issue: 5737 item_type: task @@ -25359,6 +25396,25 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:code notes: null epic: 3863 +- id: PMAT-5803 + github_issue: 5803 + item_type: task + title: 'FLOW-010 cut 1: drop aprender-compute dev-dep on aprender-core (tuner training-data tests move to aprender-core)' + status: planned + priority: high + assigned_to: null + created: 2026-10-23T10:40:00Z + updated: 2026-10-23T10:40:00Z + spec: null + acceptance_criteria: + - 'aprender-compute has no dev-dependency on aprender-core; the tuner training-data tests run from aprender-core and a guard fails if a moved test is missing, duplicated or ignored' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5923 - id: PMAT-5817 github_issue: 5817 item_type: task @@ -25534,6 +25590,25 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:measurement notes: null epic: 5510 +- id: PMAT-5909 + github_issue: 5909 + item_type: task + title: 'Crate fan-out: aprender-contracts (24 commits/30d) dirties 47 of 85 crates; split build_helper out' + status: planned + priority: medium + assigned_to: null + created: 2026-10-22T12:00:00Z + updated: 2026-10-22T12:00:00Z + spec: null + acceptance_criteria: + - See issue #5909; registered so quorum-review.sh resolves the commit trailer (EMG epic PRs) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5510 - id: PMAT-5914 github_issue: 5914 item_type: task @@ -25777,6 +25852,24 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe labels: - kind:code notes: null +- id: PMAT-6125 + github_issue: 6125 + item_type: task + title: 'fix(tree): RandomForestRegressor::fit accepts NaN labels and predicts NaN for every row' + status: planned + priority: high + assigned_to: null + created: 2026-10-23T10:50:00Z + updated: 2026-10-23T10:50:00Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5923 - id: PMAT-6131 github_issue: 6131 item_type: task diff --git a/scripts/cascade-publish.sh b/scripts/cascade-publish.sh index adb89423b2..f0e4851919 100755 --- a/scripts/cascade-publish.sh +++ b/scripts/cascade-publish.sh @@ -32,7 +32,7 @@ REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) # Tier definitions per SPEC-HF-PUBLISH-001 § crates.io release cascade. declare -A TIERS -TIERS[1]="aprender-update apr-format aprender-build-sha aprender-contracts-macros aprender-quant aprender-gemm-codegen aprender-sparse aprender-solve aprender-rand aprender-fft aprender-image aprender-tensor aprender-cupti" +TIERS[1]="aprender-update apr-format aprender-build-sha aprender-contracts-macros aprender-contracts-key aprender-quant aprender-gemm-codegen aprender-sparse aprender-solve aprender-rand aprender-fft aprender-image aprender-tensor aprender-cupti" TIERS[2]="aprender-contracts aprender-core aprender-profile-core aprender-graph aprender-contrastive-data" TIERS[3]="aprender-profile" TIERS[4]="aprender-gpu" diff --git a/scripts/check_tuner_test_move.sh b/scripts/check_tuner_test_move.sh new file mode 100755 index 0000000000..c388213ee3 --- /dev/null +++ b/scripts/check_tuner_test_move.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# check_tuner_test_move.sh -- the F041-F060 training-data-quality tests moved from aprender-compute to +# aprender-core (FLOW-010, #5803) must still be COMPILED, LISTED and NOT IGNORED by the test harness. +# BEHAVIOUR, not text: it asks cargo what each target would run, so no source-level re-gating trick +# (#[cfg], #[ignore], macro_rules! shadowing, #[path] mods, code joined onto a // line) can hide a test. +# 1. `cargo test -p aprender-core --test tuner_training_data_quality -- --list` plus +# `cargo test -p aprender-compute --test tuner_falsification -- --list` (only training_data_quality::) +# must name exactly the 11 pre-move tests, each once; a cargo failure or empty list is RED (fail closed). +# 2. the same two targets with `--list --ignored` must name none of them. +# 3. compute's Cargo.toml names no aprender-core and no compute test/example calls `aprender::`. +# 4. content pin, VERBATIM bytes (no whitespace normalising -- that let code be joined onto a `//` line): a digest of +# each moved test fn with its doc/attr lines, a digest of each file from its first item on, and the header before +# it may hold only blank/`//!`/`#![allow]`/single `use` lines (no macro_rules!). The moved bodies equal the +# merge-base bodies up to rustfmt and the dropped `#[cfg(feature = "ml-tuner")]` lines, EXCEPT f042-f044, which +# were strengthened to assert the contract unconditionally (#6125); their digests pin the strengthened text. +# F043 now exercises the library: RandomForestRegressor::fit itself refuses a non-finite label. +# Text cannot prove a test runs; parts 1-2 do. Editing a moved test means updating its digest here in the same PR. +# `--self-test` feeds synthetic cargo listings, so it needs no compile; the mutants must turn it RED. +set -u +ROOT=$(cd "$(dirname "$0")/.." && pwd) +CF=crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs +MAINF=crates/aprender-compute/tests/tuner_falsification/main.rs +KF=crates/aprender-core/tests/tuner_training_data_quality.rs +WANT="f041_empty_training_errors f042_single_sample_graceful f043_nan_labels_error f044_negative_labels_handled f045_heuristic_no_training f046_training_improves f047_large_training_no_oom f048_classifier_training f049_training_data_variance f050_feature_correlation f051_to_f060_reserved" + +DIGESTS=" +f041_empty_training_errors 070237018993271c +f042_single_sample_graceful 733fc0fa3581c5d1 +f043_nan_labels_error 44de7d832308e336 +f044_negative_labels_handled 5f44bd20d123d1b1 +f045_heuristic_no_training 8cabd670f956b0d0 +f046_training_improves f221030467215901 +f047_large_training_no_oom 140c3b8e5cb1d85b +f048_classifier_training 72c5d41797dc1285 +f049_training_data_variance 1f2fd8cbb55e4919 +f050_feature_correlation 0a494cbb1416adde +f051_to_f060_reserved fbb8c9ddc4be9944 +fit_throughput_rf 7da5ef57b0d6baeb +@tail:crates/aprender-core/tests/tuner_training_data_quality.rs 251b7174cb88652f +@tail:crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs 4268c362cc9890b0 +@file:crates/aprender-compute/tests/tuner_falsification/main.rs 2a75089411202f35 +" + +# body FILE NAME -> VERBATIM text of `fn NAME(` with its leading `///` and `#[..]` lines, up to its closing `}` at column 0. +# No whitespace is stripped: stripping newlines is what let code be joined onto a `//` line and still hash equal. +body() { + awk -v n="$2" '/^(\/\/\/|#\[)/ { pend = pend $0 "\n"; next } + $0 ~ "^fn "n"\\(" { printf "%s", pend; p = 1 } + p { print } p && /^}/ { exit } + { pend = "" }' "$1" +} +# tail FILE -> everything from the first item (`///`, `#[test]` or `fn`) on; the header before it is checked, not hashed +tailof() { awk '!t && /^(\/\/\/|#\[test\]|fn )/ { t = 1 } t { print }' "$1"; } +# header_ok FILE -> rc 0 only when every line before the first item is blank, `//!`, `#![allow(..)]`, `#[allow(unused_imports)]` +# or ONE single-statement `use trueno|aprender::..;` -- so no macro_rules!, mod, #[path] or fn can hide in the header +header_ok() { awk '/^(\/\/\/|#\[test\]|fn )/ { exit } { print }' "$1" | /usr/bin/grep -v -E '^$|^//!|^#!\[allow\([a-z_:, ]*\)\]$|^#\[allow\(unused_imports\)\]$|^use (trueno|aprender)::[^;]*;$' | { ! /usr/bin/grep -q .; }; } +# listed FILE KIND -> sorted names of the moved tests in a `--list` output (KIND core: every test; comp: training_data_quality:: only) +listed() { + awk -v kind="$2" '/: test$/ { n = $0; sub(/: test$/, "", n) + if (kind == "comp") { if (n !~ /^training_data_quality::/) next; sub(/^training_data_quality::/, "", n) } + print n }' "$1" 2> /dev/null | sort +} +# verdict_lists CORE COMP CORE_IGN COMP_IGN -> problems; rc 0 only when none +verdict_lists() { + local bad=0 got want x + want=$(printf '%s\n' $WANT | sort) + for x in "$1" "$2"; do [ -s "$x" ] || { echo "empty or missing cargo listing ($x): fail closed"; bad=1; }; done + got=$({ listed "$1" core; listed "$2" comp; } | sort) + [ "$got" = "$want" ] || { echo "listed tests differ from the 11 pre-move names: got: $(echo $got)"; bad=1; } + got=$({ listed "$3" core; listed "$4" comp; } | sort) + [ -z "$got" ] || { echo "ignored moved tests: $(echo $got)"; bad=1; } + return $bad +} +# verdict_static ROOT -> Cargo dependency, aprender:: calls, content pin +verdict_static() { + local r=$1 bad=0 n d b + while read -r n d; do + [ -n "$n" ] || continue + case $n in + @tail:*) b=$(tailof "$r/${n#@tail:}") ;; + @file:*) b=$(awk 1 "$r/${n#@file:}" 2> /dev/null) ;; + *) b=$(body "$r/$CF" "$n"); [ -n "$b" ] || b=$(body "$r/$KF" "$n") ;; + esac + [ "$(printf '%s\n' "$b" | sha256sum | cut -c1-16)" = "$d" ] || { echo "$n differs from its pinned verbatim digest (empty or edited)"; bad=1; } + done <<< "$DIGESTS" + for b in "$CF" "$KF"; do header_ok "$r/$b" || { echo "$b: header before the first item holds more than blank, //!, #![allow], use lines"; bad=1; }; done + for b in aprender-compute aprender-core; do # no [[test]] may redirect/disable either moved target, no autotests = false + awk '/^autotests[[:space:]]*=/ { print "autotests set"; } + /^\[\[test\]\]/ { t = 1; next } /^\[/ { t = 0 } + t && /^name[[:space:]]*=[[:space:]]*"(tuner_falsification|tuner_training_data_quality)"/ { print "[[test]] names a moved target" }' "$r/crates/$b/Cargo.toml" | /usr/bin/grep -q . && { echo "crates/$b/Cargo.toml: autotests set or a [[test]] redirects a moved target"; bad=1; } + done + if grep -v '^[[:space:]]*#' "$r/crates/aprender-compute/Cargo.toml" | grep -q 'aprender-core'; then echo "compute Cargo.toml names aprender-core"; bad=1; fi + if grep -rn --include='*.rs' -E '\baprender::' "$r/crates/aprender-compute/tests" "$r/crates/aprender-compute/examples" 2>/dev/null | grep -v -E '^[^:]+:[0-9]+:[[:space:]]*//' | grep -q .; then + echo "compute tests/examples call aprender::"; bad=1 + fi + return $bad +} +# cargo_lists ROOT DIR -> writes DIR/{core,comp,core_ign,comp_ign}.txt from cargo; a cargo failure leaves the file empty (RED) +cargo_lists() { + local r=$1 d=$2 + ( env -C "$r" cargo test --quiet -p aprender-core --test tuner_training_data_quality -- --list 2> /dev/null ) > "$d/core.txt" || : > "$d/core.txt" + ( env -C "$r" cargo test --quiet -p aprender-compute --test tuner_falsification -- --list 2> /dev/null ) > "$d/comp.txt" || : > "$d/comp.txt" + ( env -C "$r" cargo test --quiet -p aprender-core --test tuner_training_data_quality -- --list --ignored 2> /dev/null ) > "$d/core_ign.txt" || echo FAILED > "$d/core_ign.txt" + ( env -C "$r" cargo test --quiet -p aprender-compute --test tuner_falsification -- --list --ignored 2> /dev/null ) > "$d/comp_ign.txt" || echo FAILED > "$d/comp_ign.txt" +} +verdict() { # ROOT -> problems; rc 0 only when none + local r=$1 d bad=0 + d=$(mktemp -d) || return 2 + cargo_lists "$r" "$d" + [ "$(cat "$d/core_ign.txt")" = FAILED ] || [ "$(cat "$d/comp_ign.txt")" = FAILED ] && { echo "cargo --list --ignored failed: fail closed"; bad=1; } + verdict_lists "$d/core.txt" "$d/comp.txt" "$d/core_ign.txt" "$d/comp_ign.txt" || bad=1 + verdict_static "$r" || bad=1 + rm -rf "${d:?}" + return $bad +} +if [ "${1:-}" = "--self-test" ]; then + t=$(mktemp -d) || exit 2; trap 'rm -rf "${t:?}"' EXIT + n=0; f=0 + mkl() { # DIR -> a good set of listings + mkdir -p "$t/$1"; : > "$t/$1/core.txt"; : > "$t/$1/comp.txt"; : > "$t/$1/ci.txt"; : > "$t/$1/pi.txt" + for w in $WANT; do case $w in f041*|f042*|f043*|f044*|f046*|f047*|f048*) echo "$w: test" >> "$t/$1/core.txt" ;; *) echo "training_data_quality::$w: test" >> "$t/$1/comp.txt" ;; esac; done + echo "other::unrelated: test" >> "$t/$1/comp.txt"; echo "0 tests, 0 benchmarks" >> "$t/$1/ci.txt"; echo "0 tests, 0 benchmarks" >> "$t/$1/pi.txt"; } + lcase() { # NAME WANT_RC DIR + ( verdict_lists "$t/$3/core.txt" "$t/$3/comp.txt" "$t/$3/ci.txt" "$t/$3/pi.txt" > /dev/null ); rc=$? + if { [ "$2" = 0 ] && [ $rc -eq 0 ]; } || { [ "$2" != 0 ] && [ $rc -ne 0 ]; }; then n=$((n+1)); echo "ok $1"; else f=$((f+1)); echo "FAIL $1 (rc=$rc)"; fi; } + mkl g; lcase "L1 good listing: the 11 once each, none ignored" 0 g + mkl a; sed -i '/f041_/d' "$t/a/core.txt"; lcase "L2 MUTANT: a moved test not listed (cfg'd out / shadowed / #[path]'d)" 1 a + mkl b; echo "f041_empty_training_errors: test" >> "$t/b/core.txt"; lcase "L3 MUTANT: a test listed twice" 1 b + mkl c; echo "f046_training_improves: test" > "$t/c/ci.txt"; lcase "L4 MUTANT: a moved test is #[ignore]d (in --ignored)" 1 c + mkl d; : > "$t/d/comp.txt"; lcase "L5 MUTANT: compute listing empty (cargo failed): fail closed" 1 d + mkl e; sed -i '/f049_/d' "$t/e/comp.txt"; lcase "L6 MUTANT: compute target lost its module (CI step off / mod removed)" 1 e + mkl h; echo "training_data_quality::f052_extra: test" >> "$t/h/comp.txt"; lcase "L7 MUTANT: an unaccounted test in the moved module" 1 h + mkl i; echo "training_data_quality::f045_heuristic_no_training: test" > "$t/i/pi.txt"; lcase "L8 MUTANT: a compute test is ignored" 1 i + # static rows run on copies of the real files + mk() { mkdir -p "$t/$1/crates/aprender-compute/tests/tuner_falsification" "$t/$1/crates/aprender-compute/examples" "$t/$1/crates/aprender-core/tests" + cp "$ROOT/$CF" "$t/$1/$CF"; cp "$ROOT/$KF" "$t/$1/$KF"; cp "$ROOT/crates/aprender-compute/Cargo.toml" "$t/$1/crates/aprender-compute/Cargo.toml" + mkdir -p "$t/$1/crates/aprender-core"; cp "$ROOT/crates/aprender-core/Cargo.toml" "$t/$1/crates/aprender-core/Cargo.toml" + cp "$ROOT/$MAINF" "$t/$1/$MAINF"; } + scase() { ( verdict_static "$t/$3" > /dev/null ); rc=$? + if { [ "$2" = 0 ] && [ $rc -eq 0 ]; } || { [ "$2" != 0 ] && [ $rc -ne 0 ]; }; then n=$((n+1)); echo "ok $1"; else f=$((f+1)); echo "FAIL $1 (rc=$rc)"; fi; } + mk s0; scase "S1 current tree: pins hold, compute has no aprender-core" 0 s0 + mk s1; sed -i 's/F046 FALSIFIED/F046 weakened/' "$t/s1/$KF"; scase "S2 MUTANT: a moved test body edited" 1 s1 + mk s2; sed -i '/fn f044_negative_labels_handled/,/^}/{s/\.build()/.build(); let _ = 0/}' "$t/s2/$KF"; scase "S3 MUTANT: a moved test body changed in logic" 1 s2 + mk s3; printf '[dev-dependencies]\naprender = { path = "../aprender-core", package = "aprender-core" }\n' >> "$t/s3/crates/aprender-compute/Cargo.toml"; scase "S4 MUTANT: dev-dep on aprender-core back" 1 s3 + mk s4; printf 'use aprender::tree::RandomForestRegressor;\n' > "$t/s4/crates/aprender-compute/examples/x.rs"; scase "S5 MUTANT: compute example calls aprender::" 1 s4 + mk s5; sed -i 's/< 10/< 11/' "$t/s5/$KF"; scase "S6 MUTANT: helper fit_throughput_rf guard changed" 1 s5 + mk s6; sed -i '/Generate training data that matches heuristic pattern/{N;s/\n */ /}' "$t/s6/$KF"; scase "S7 MUTANT: code joined onto a // line (the r3/r4 hole)" 1 s6 + mk s7; sed -i '/^use trueno::tuner::TunerFeatures;/a macro_rules! assert { ($($t:tt)*) => {}; }' "$t/s7/$KF"; scase "S8 MUTANT: macro_rules! assert shadow after the use line" 1 s7 + mk s8; sed -i '/^use trueno::tuner::TunerFeatures;/a #[path = "elsewhere.rs"] mod shadow;' "$t/s8/$KF"; scase "S9 MUTANT: #[path] mod in the header" 1 s8 + mk s9; sed -i 's/F045 FALSIFIED/F045 weakened/' "$t/s9/$CF"; scase "S10 MUTANT: a compute-resident test body edited" 1 s9 + mk s10; printf '\n#[test]\nfn f099_extra() {}\n' >> "$t/s10/$KF"; scase "S11 MUTANT: an extra test appended to the moved file (tail digest)" 1 s10 + mk s11; sed -i '1s/$/ macro_rules! assert { ($($t:tt)*) => {}; }/' "$t/s11/$KF"; scase "S12 MUTANT: code on the #![allow] header line (end-anchor)" 1 s11 + mk s12; sed -i 's/^mod training_data_quality;/#[path = "elsewhere.rs"] mod training_data_quality;/' "$t/s12/$MAINF"; scase "S13 MUTANT: compute main.rs redirects the module" 1 s12 + mk s13; printf '\n[[test]]\nname = "tuner_falsification"\npath = "tests/elsewhere.rs"\n' >> "$t/s13/crates/aprender-compute/Cargo.toml"; scase "S14 MUTANT: [[test]] redirects the compute target" 1 s13 + mk s14; printf '\n[[test]]\nname = "tuner_training_data_quality"\npath = "tests/elsewhere.rs"\n' >> "$t/s14/crates/aprender-core/Cargo.toml"; scase "S15 MUTANT: [[test]] redirects the core target" 1 s14 + echo "tuner test move rows: $n passed, $f failed"; [ $f -eq 0 ]; exit +fi +out=$(verdict "${1:-$ROOT}") && { echo "tuner test move: cargo lists the 11 moved tests once each, none ignored; compute has no aprender-core"; exit 0; } +printf '%s\n' "$out"; exit 1 diff --git a/scripts/guards_nightly_manifest.txt b/scripts/guards_nightly_manifest.txt index fd9cd3d0fd..a0841624ad 100644 --- a/scripts/guards_nightly_manifest.txt +++ b/scripts/guards_nightly_manifest.txt @@ -49,6 +49,8 @@ # #4709 (C303): the pv census ratchet, LAB from its first night (never on a PR path, C324). # Seconds are a local wall-clock sample (2026-10-14, pv prebuilt); the first nightly replaces them. 6 The pv census bound share may only rise (#4709) +# #5502: parity-receipt count vs EXPECTED_RECEIPTS (verify mode), LAB, nightly only (C324); seconds are a guess until the first night. +5 Parity-receipt count equals EXPECTED_RECEIPTS (verify mode, #5502, LAB) # #5873: the harness fixture-server cleanup check, quarantined from ci/explicit-test-commands.d/817 (its apr-stub row races); LAB, never on a PR path (C324). 60 Harness fixture servers die with their harness (LAB, quarantined from the merge path, #5873) 1 affected_set.sh case table and mutants (FLOW-010 E5, LAB) @@ -56,3 +58,5 @@ 5 Coverage scan files no per-function ticket (LAB, #5879) # #5809 (FLOW-010 E5): affected-set tool case tables, LAB from the first night (never on a PR path, C324). Seconds are a local sample. 40 Affected-set tool and readers guards (FLOW-010 E5, #5809) +# #5803: tuner test-move guard (cargo -- --list), LAB from the first night, nightly only (never on a PR path, C324); the moved tests run from ci/explicit-test-commands.d 295 and 298. Seconds are a local sample. +60 Tuner test-move guard (#5803) diff --git a/scripts/release/publish-order.txt b/scripts/release/publish-order.txt index 33751e194b..8bab042ce2 100644 --- a/scripts/release/publish-order.txt +++ b/scripts/release/publish-order.txt @@ -3,6 +3,7 @@ aprender-update apr-format aprender-common aprender-contracts-macros +aprender-contracts-key aprender-cupti aprender-data aprender-fft