diff --git a/Cargo.lock b/Cargo.lock index d1c6569f81..31fae84c16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,7 +453,6 @@ dependencies = [ "anyhow", "aprender-contracts", "aprender-contracts-macros", - "aprender-core", "aprender-cuda-edge", "aprender-gemm-codegen", "aprender-gpu", diff --git a/Cargo.toml b/Cargo.toml index 71b08df5c4..ddab060b4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -640,6 +640,11 @@ include = [ # after-the-binary phase. [package.metadata.dogfood] gates = [ + # #5803/#5804 — the moved tuner tests must still be listed by cargo exactly once, none ignored + # (behaviour guard: cargo test -- --list). It invokes cargo, so guard_tree --no-cargo never + # runs it; ci/explicit-test-commands.d/296-298 run it in the full tier (tags/release), and this wires it for the + # meta-guard (#4005 ruling C). + "scripts/check_tuner_test_move.sh", "scripts/check_multiplatform_dogfood.sh", # #3477 — every claimed (architecture, backend, silicon) triple has a per-host receipt # from scripts/model_ladder.sh; dense Qwen3 on CUDA shipped broken in 0.68.1 because no 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/296-tuner-test-move-5803.cmd b/ci/explicit-test-commands.d/296-tuner-test-move-5803.cmd new file mode 100644 index 0000000000..a80e48e8f0 --- /dev/null +++ b/ci/explicit-test-commands.d/296-tuner-test-move-5803.cmd @@ -0,0 +1 @@ +bash scripts/check_tuner_test_move.sh --self-test diff --git a/ci/explicit-test-commands.d/297-tuner-test-move-tree-5803.cmd b/ci/explicit-test-commands.d/297-tuner-test-move-tree-5803.cmd new file mode 100644 index 0000000000..954e531e01 --- /dev/null +++ b/ci/explicit-test-commands.d/297-tuner-test-move-tree-5803.cmd @@ -0,0 +1 @@ +bash scripts/check_tuner_test_move.sh 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/crates/aprender-compute/Cargo.toml b/crates/aprender-compute/Cargo.toml index 1aa9e9f4ab..9ad20f825f 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 @@ -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,10 +134,6 @@ 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). -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 # (to_dot/to_ascii_tree, slowest-path) is always available, no graph crate needed. 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..5d53942515 100644 --- a/crates/aprender-compute/book/src/performance/ml-tuner.md +++ b/crates/aprender-compute/book/src/performance/ml-tuner.md @@ -82,72 +82,14 @@ 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 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 +120,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/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-core/tests/tuner_training_data_quality.rs b/crates/aprender-core/tests/tuner_training_data_quality.rs new file mode 100644 index 0000000000..41960a5b1f --- /dev/null +++ b/crates/aprender-core/tests/tuner_training_data_quality.rs @@ -0,0 +1,171 @@ +#![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)]; + // Should error (insufficient), not panic + let _ = fit_throughput_rf(&data, 10); +} + +/// F043: Training with NaN labels should not produce NaN predictions +#[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 +#[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); +} + +/// 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/scripts/check_tuner_test_move.sh b/scripts/check_tuner_test_move.sh new file mode 100755 index 0000000000..e081c6dbb0 --- /dev/null +++ b/scripts/check_tuner_test_move.sh @@ -0,0 +1,157 @@ +#!/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 (proved in the PR body). +# 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 bc88d040de7f6792 +f043_nan_labels_error 9fd06ac9384bfcaa +f044_negative_labels_handled fbb01296c746a127 +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 e599e682001e6f49 +@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=$(cat "$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 + ( cd "$r" && cargo test --quiet -p aprender-core --test tuner_training_data_quality -- --list 2> /dev/null ) > "$d/core.txt" || : > "$d/core.txt" + ( cd "$r" && cargo test --quiet -p aprender-compute --test tuner_falsification -- --list 2> /dev/null ) > "$d/comp.txt" || : > "$d/comp.txt" + ( cd "$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" + ( cd "$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 '/Should error (insufficient), not panic/{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