## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/trueno/src/tuner/brick_tuner.rs

    Line
    Count
    Source
    1
    //! BrickTuner - ML-based ComputeBrick Tuner Ensemble
    2
    //!
    3
    //! Combines throughput regression, kernel classification, and bottleneck analysis.
    4
    5
    use serde::{Deserialize, Serialize};
    6
    7
    use super::error::TunerError;
    8
    use super::features::TunerFeatures;
    9
    use super::helpers::{chrono_lite_now, crc32_update, pad_right};
    10
    use super::models::{
    11
        BottleneckClassifier, BottleneckPrediction, KernelClassifier, KernelRecommendation,
    12
        ThroughputPrediction, ThroughputRegressor,
    13
    };
    14
    use super::types::{BottleneckClass, KernelType};
    15
    16
    // ============================================================================
    17
    // TunerRecommendation
    18
    // ============================================================================
    19
    20
    /// Combined tuner recommendation
    21
    #[derive(Debug, Clone, Serialize, Deserialize)]
    22
    pub struct TunerRecommendation {
    23
        /// Throughput prediction
    24
        pub throughput: ThroughputPrediction,
    25
        /// Kernel recommendation
    26
        pub kernel: KernelRecommendation,
    27
        /// Bottleneck analysis
    28
        pub bottleneck: BottleneckPrediction,
    29
        /// Model version
    30
        pub model_version: String,
    31
        /// Overall confidence
    32
        pub confidence_overall: f32,
    33
        /// Suggested experiments to try
    34
        pub suggested_experiments: Vec<ExperimentSuggestion>,
    35
    }
    36
    37
    /// Suggested experiment to improve performance
    38
    #[derive(Debug, Clone, Serialize, Deserialize)]
    39
    pub enum ExperimentSuggestion {
    40
        /// Increase batch size
    41
        IncreaseBatchSize { from: u32, to: u32 },
    42
        /// Enable CUDA graphs
    43
        EnableCudaGraphs,
    44
        /// Try a specific kernel
    45
        TryKernel { kernel: KernelType },
    46
        /// Reduce sequence length
    47
        ReduceSequenceLength { factor: f32 },
    48
        /// Enable multi-KV cache
    49
        EnableMultiKvCache { count: u32 },
    50
    }
    51
    52
    impl std::fmt::Display for ExperimentSuggestion {
    53
    0
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    54
    0
            match self {
    55
    0
                ExperimentSuggestion::IncreaseBatchSize { from, to } => {
    56
    0
                    write!(f, "Increase batch size: M={} → M={}", from, to)
    57
                }
    58
                ExperimentSuggestion::EnableCudaGraphs => {
    59
    0
                    write!(f, "Enable CUDA graphs for kernel launch amortization")
    60
                }
    61
    0
                ExperimentSuggestion::TryKernel { kernel } => {
    62
    0
                    write!(f, "Try kernel: {:?}", kernel)
    63
                }
    64
    0
                ExperimentSuggestion::ReduceSequenceLength { factor } => {
    65
    0
                    write!(
    66
    0
                        f,
    67
    0
                        "Reduce sequence length by {:.0}%",
    68
    0
                        (1.0 - factor) * 100.0
    69
                    )
    70
                }
    71
    0
                ExperimentSuggestion::EnableMultiKvCache { count } => {
    72
    0
                    write!(
    73
    0
                        f,
    74
    0
                        "Enable {} separate KV caches for batched attention",
    75
                        count
    76
                    )
    77
                }
    78
            }
    79
    0
        }
    80
    }
    81
    82
    // ============================================================================
    83
    // BrickTuner
    84
    // ============================================================================
    85
    86
    /// ML-based ComputeBrick tuner ensemble.
    87
    ///
    88
    /// Combines three models for comprehensive recommendations:
    89
    /// - ThroughputRegressor: Predicts tok/s
    90
    /// - KernelClassifier: Selects best kernel
    91
    /// - BottleneckClassifier: Identifies performance bottleneck
    92
    #[derive(Debug, Clone, Serialize, Deserialize)]
    93
    pub struct BrickTuner {
    94
        /// Throughput regression model
    95
        pub(crate) throughput: ThroughputRegressor,
    96
        /// Kernel classification model
    97
        pub(crate) kernel: KernelClassifier,
    98
        /// Bottleneck classification model
    99
        pub(crate) bottleneck: BottleneckClassifier,
    100
        /// Model version
    101
        pub(crate) version: String,
    102
        /// Training timestamp
    103
        pub(crate) trained_at: String,
    104
        /// Number of training samples
    105
        pub(crate) sample_count: usize,
    106
    }
    107
    108
    impl Default for BrickTuner {
    109
    0
        fn default() -> Self {
    110
    0
            Self::new()
    111
    0
        }
    112
    }
    113
    114
    impl BrickTuner {
    115
        /// Model version
    116
        pub const VERSION: &'static str = "1.0.0";
    117
    118
        /// APR format magic bytes (APR1 = uncompressed)
    119
        const APR_MAGIC: [u8; 4] = [b'A', b'P', b'R', b'1'];
    120
    121
        /// Create a new tuner with default models
    122
    0
        pub fn new() -> Self {
    123
    0
            Self {
    124
    0
                throughput: ThroughputRegressor::new(),
    125
    0
                kernel: KernelClassifier::new(),
    126
    0
                bottleneck: BottleneckClassifier::new(),
    127
    0
                version: Self::VERSION.to_string(),
    128
    0
                trained_at: chrono_lite_now(),
    129
    0
                sample_count: 0,
    130
    0
            }
    131
    0
        }
    132
    133
        /// Get the model version string
    134
    0
        pub fn version(&self) -> &str {
    135
    0
            &self.version
    136
    0
        }
    137
    138
        /// Get the throughput regressor's MAPE (Mean Absolute Percentage Error)
    139
    0
        pub fn throughput_mape(&self) -> f32 {
    140
    0
            self.throughput.mape
    141
    0
        }
    142
    143
        /// Get the number of training samples used
    144
    0
        pub fn throughput_sample_count(&self) -> usize {
    145
    0
            self.throughput.sample_count
    146
    0
        }
    147
    148
        /// Get comprehensive tuning recommendation
    149
    0
        pub fn recommend(&self, features: &TunerFeatures) -> TunerRecommendation {
    150
    0
            let throughput = self.throughput.predict(features);
    151
    0
            let kernel = self.kernel.predict(features);
    152
    0
            let bottleneck = self.bottleneck.predict(features);
    153
    154
            // Calculate overall confidence
    155
    0
            let confidence_overall =
    156
    0
                (throughput.confidence + kernel.confidence + bottleneck.confidence) / 3.0;
    157
    158
            // Generate experiment suggestions based on bottleneck
    159
    0
            let suggested_experiments = self.suggest_experiments(features, &bottleneck);
    160
    161
    0
            TunerRecommendation {
    162
    0
                throughput,
    163
    0
                kernel,
    164
    0
                bottleneck,
    165
    0
                model_version: self.version.clone(),
    166
    0
                confidence_overall,
    167
    0
                suggested_experiments,
    168
    0
            }
    169
    0
        }
    170
    171
        /// Suggest experiments based on current bottleneck
    172
    0
        pub fn suggest_experiments(
    173
    0
            &self,
    174
    0
            features: &TunerFeatures,
    175
    0
            bottleneck: &BottleneckPrediction,
    176
    0
        ) -> Vec<ExperimentSuggestion> {
    177
    0
            let mut suggestions = Vec::new();
    178
    0
            let batch_size = (features.batch_size_norm * 64.0).round() as u32;
    179
    180
    0
            match bottleneck.class {
    181
                BottleneckClass::MemoryBound => {
    182
    0
                    if batch_size < 8 {
    183
    0
                        suggestions.push(ExperimentSuggestion::IncreaseBatchSize {
    184
    0
                            from: batch_size,
    185
    0
                            to: (batch_size * 2).min(8),
    186
    0
                        });
    187
    0
                    }
    188
    0
                    suggestions.push(ExperimentSuggestion::TryKernel {
    189
    0
                        kernel: KernelType::BatchedQ4K,
    190
    0
                    });
    191
    0
                    if batch_size > 1 {
    192
    0
                        suggestions.push(ExperimentSuggestion::EnableMultiKvCache { count: batch_size });
    193
    0
                    }
    194
                }
    195
                BottleneckClass::LaunchBound => {
    196
    0
                    if features.cuda_graphs < 0.5 {
    197
    0
                        suggestions.push(ExperimentSuggestion::EnableCudaGraphs);
    198
    0
                    }
    199
    0
                    suggestions.push(ExperimentSuggestion::TryKernel {
    200
    0
                        kernel: KernelType::FusedRmsNormQ4K,
    201
    0
                    });
    202
                }
    203
    0
                BottleneckClass::AttentionBound => {
    204
    0
                    suggestions.push(ExperimentSuggestion::TryKernel {
    205
    0
                        kernel: KernelType::BatchedAttention,
    206
    0
                    });
    207
    0
                    suggestions.push(ExperimentSuggestion::ReduceSequenceLength { factor: 0.5 });
    208
    0
                }
    209
                _ => {
    210
                    // Default suggestions
    211
    0
                    if batch_size < 4 {
    212
    0
                        suggestions.push(ExperimentSuggestion::IncreaseBatchSize {
    213
    0
                            from: batch_size,
    214
    0
                            to: 4,
    215
    0
                        });
    216
    0
                    }
    217
                }
    218
            }
    219
    220
    0
            suggestions
    221
    0
        }
    222
    223
        /// Train all models on labeled data
    224
    0
        pub fn train(&mut self, data: &[(TunerFeatures, f32)]) -> Result<(), TunerError> {
    225
    0
            self.throughput.train(data)?;
    226
    0
            self.sample_count = data.len();
    227
    0
            self.trained_at = chrono_lite_now();
    228
    0
            Ok(())
    229
    0
        }
    230
    231
        /// Print recommendations to console (TUI-friendly)
    232
    0
        pub fn print_recommendation(&self, rec: &TunerRecommendation) {
    233
    0
            println!("╭─────────────────────────────────────────────────────────────╮");
    234
    0
            println!(
    235
    0
                "│           BrickTuner Recommendations v{}                 │",
    236
                self.version
    237
            );
    238
    0
            println!("├─────────────────────────────────────────────────────────────┤");
    239
    0
            println!(
    240
    0
                "│ Predicted throughput: {:>7.1} tok/s ({:>4.0}% confidence)     │",
    241
                rec.throughput.predicted_tps,
    242
    0
                rec.throughput.confidence * 100.0
    243
            );
    244
    0
            println!(
    245
    0
                "│ Recommended kernel:   {:>15?} ({:>4.0}% conf)       │",
    246
                rec.kernel.top_kernel,
    247
    0
                rec.kernel.confidence * 100.0
    248
            );
    249
    0
            println!(
    250
    0
                "│ Bottleneck class:     {:>15} ({:>4.0}% conf)       │",
    251
                rec.bottleneck.class,
    252
    0
                rec.bottleneck.confidence * 100.0
    253
            );
    254
    0
            println!("├─────────────────────────────────────────────────────────────┤");
    255
    0
            println!(
    256
    0
                "│ Explanation: {}│",
    257
    0
                pad_right(&rec.bottleneck.explanation, 47)
    258
            );
    259
    0
            println!("├─────────────────────────────────────────────────────────────┤");
    260
    0
            println!("│ Suggested experiments:                                      │");
    261
    0
            for (i, exp) in rec.suggested_experiments.iter().take(3).enumerate() {
    262
    0
                println!("│  {}. {}│", i + 1, pad_right(&exp.to_string(), 56));
    263
    0
            }
    264
    0
            println!("╰─────────────────────────────────────────────────────────────╯");
    265
    0
        }
    266
    267
        // ========================================================================
    268
        // T-TUNER-006: cbtop TUI Integration (GitHub #83)
    269
        // ========================================================================
    270
    271
        /// Render recommendation as TUI panel lines (for cbtop integration)
    272
        ///
    273
        /// Returns a vector of strings that can be rendered in a TUI widget.
    274
        /// Each line is formatted for fixed-width display (width=61 chars).
    275
    0
        pub fn render_panel(&self, rec: &TunerRecommendation) -> Vec<String> {
    276
    0
            let mut lines = Vec::with_capacity(12);
    277
    278
    0
            lines.push(format!(
    279
    0
                "│           BrickTuner Recommendations v{}                 │",
    280
                self.version
    281
            ));
    282
    0
            lines.push(
    283
    0
                "├─────────────────────────────────────────────────────────────┤".to_string(),
    284
            );
    285
    0
            lines.push(format!(
    286
    0
                "│ Predicted throughput: {:>7.1} tok/s ({:>4.0}% confidence)     │",
    287
                rec.throughput.predicted_tps,
    288
    0
                rec.throughput.confidence * 100.0
    289
            ));
    290
    0
            lines.push(format!(
    291
    0
                "│ Recommended kernel:   {:>15?} ({:>4.0}% conf)       │",
    292
                rec.kernel.top_kernel,
    293
    0
                rec.kernel.confidence * 100.0
    294
            ));
    295
    0
            lines.push(format!(
    296
    0
                "│ Bottleneck class:     {:>15} ({:>4.0}% conf)       │",
    297
                rec.bottleneck.class,
    298
    0
                rec.bottleneck.confidence * 100.0
    299
            ));
    300
    0
            lines.push(
    301
    0
                "├─────────────────────────────────────────────────────────────┤".to_string(),
    302
            );
    303
    0
            lines.push(format!(
    304
    0
                "│ Explanation: {}│",
    305
    0
                pad_right(&rec.bottleneck.explanation, 47)
    306
            ));
    307
    0
            lines.push(
    308
    0
                "├─────────────────────────────────────────────────────────────┤".to_string(),
    309
            );
    310
    0
            lines.push(
    311
    0
                "│ Suggested experiments:                                      │".to_string(),
    312
            );
    313
    314
    0
            for (i, exp) in rec.suggested_experiments.iter().take(3).enumerate() {
    315
    0
                lines.push(format!(
    316
    0
                    "│  {}. {}│",
    317
    0
                    i + 1,
    318
    0
                    pad_right(&exp.to_string(), 56)
    319
    0
                ));
    320
    0
            }
    321
    322
            // Pad if fewer than 3 suggestions
    323
    0
            for _ in rec.suggested_experiments.len()..3 {
    324
    0
                lines.push(
    325
    0
                    "│                                                             │".to_string(),
    326
    0
                );
    327
    0
            }
    328
    329
    0
            lines.push(
    330
    0
                "├─────────────────────────────────────────────────────────────┤".to_string(),
    331
            );
    332
    0
            lines.push(
    333
    0
                "│ [Press 'a' to apply] [Press 't' to toggle] [Press 'r' to run]│".to_string(),
    334
            );
    335
    336
    0
            lines
    337
    0
        }
    338
    339
        /// Render compact recommendation (single line for status bar)
    340
    0
        pub fn render_compact(&self, rec: &TunerRecommendation) -> String {
    341
    0
            format!(
    342
    0
                "Tuner: {:.0} tok/s | {:?} | {} ({:.0}%)",
    343
                rec.throughput.predicted_tps,
    344
                rec.kernel.top_kernel,
    345
                rec.bottleneck.class,
    346
    0
                rec.confidence_overall * 100.0
    347
            )
    348
    0
        }
    349
    350
        /// Render prediction vs actual comparison
    351
    0
        pub fn render_comparison(&self, rec: &TunerRecommendation, actual_tps: f32) -> Vec<String> {
    352
    0
            let error_pct = if actual_tps > 0.0 {
    353
    0
                ((rec.throughput.predicted_tps - actual_tps) / actual_tps * 100.0).abs()
    354
            } else {
    355
    0
                0.0
    356
            };
    357
    358
    0
            let accuracy_indicator = if error_pct < 5.0 {
    359
    0
                "🎯 Excellent"
    360
    0
            } else if error_pct < 10.0 {
    361
    0
                "✓ Good"
    362
    0
            } else if error_pct < 20.0 {
    363
    0
                "△ Fair"
    364
            } else {
    365
    0
                "✗ Poor"
    366
            };
    367
    368
    0
            vec![
    369
    0
                format!(
    370
    0
                    "│ Predicted: {:>7.1} tok/s  Actual: {:>7.1} tok/s           │",
    371
                    rec.throughput.predicted_tps, actual_tps
    372
                ),
    373
    0
                format!(
    374
    0
                    "│ Error: {:>5.1}%  Accuracy: {:>12}                       │",
    375
                    error_pct, accuracy_indicator
    376
                ),
    377
            ]
    378
    0
        }
    379
    380
        /// Serialize to JSON
    381
    0
        pub fn to_json(&self) -> Result<String, TunerError> {
    382
    0
            serde_json::to_string_pretty(self).map_err(|e| TunerError::Serialization(e.to_string()))
    383
    0
        }
    384
    385
        /// Deserialize from JSON
    386
    0
        pub fn from_json(json: &str) -> Result<Self, TunerError> {
    387
    0
            serde_json::from_str(json).map_err(|e| TunerError::Serialization(e.to_string()))
    388
    0
        }
    389
    390
        // =========================================================================
    391
        // APR Persistence (SOVEREIGN STACK - GH#81)
    392
        // =========================================================================
    393
    394
        /// Get the default cache path for tuner models.
    395
        ///
    396
        /// Returns `~/.cache/trueno/tuner_model_v{VERSION}.apr`
    397
        #[cfg(feature = "hardware-detect")]
    398
        pub fn cache_path() -> std::path::PathBuf {
    399
            let cache_dir = dirs::cache_dir()
    400
                .unwrap_or_else(|| std::path::PathBuf::from("."))
    401
                .join("trueno");
    402
    403
            // Create directory if it doesn't exist
    404
            let _ = std::fs::create_dir_all(&cache_dir);
    405
    406
            cache_dir.join(format!("tuner_model_v{}.apr", Self::VERSION))
    407
        }
    408
    409
        /// Load tuner from cache or create new with defaults.
    410
        ///
    411
        /// This is the recommended way to create a BrickTuner for production use.
    412
        /// It will:
    413
        /// 1. Check for cached model at `~/.cache/trueno/tuner_model_v{VERSION}.apr`
    414
        /// 2. Load if exists and version matches
    415
        /// 3. Create new with defaults if not found or version mismatch
    416
        #[cfg(feature = "hardware-detect")]
    417
        pub fn load_or_default() -> Self {
    418
            let path = Self::cache_path();
    419
    420
            if path.exists() {
    421
                match Self::load_apr(&path) {
    422
                    Ok(tuner) => {
    423
                        // Version check
    424
                        if tuner.version == Self::VERSION {
    425
                            return tuner;
    426
                        }
    427
                        // Version mismatch - create new
    428
                    }
    429
                    Err(_) => {
    430
                        // Load failed - create new
    431
                    }
    432
                }
    433
            }
    434
    435
            Self::new()
    436
        }
    437
    438
        /// Save tuner model to .apr file.
    439
        ///
    440
        /// APR1 format (uncompressed):
    441
        /// - 4-byte magic: "APR1"
    442
        /// - 4-byte metadata_len: u32 LE
    443
        /// - JSON metadata
    444
        /// - 4-byte CRC32: checksum
    445
    0
        pub fn save_apr<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), TunerError> {
    446
            use std::io::Write;
    447
    448
    0
            let json = self.to_json()?;
    449
    0
            let json_bytes = json.as_bytes();
    450
    451
    0
            let mut file =
    452
    0
                std::fs::File::create(path).map_err(|e| TunerError::Io(e.to_string()))?;
    453
    454
            // Write magic
    455
    0
            file.write_all(&Self::APR_MAGIC)
    456
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    457
    458
            // Write metadata length
    459
    0
            let len = json_bytes.len() as u32;
    460
    0
            file.write_all(&len.to_le_bytes())
    461
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    462
    463
            // Write JSON metadata
    464
    0
            file.write_all(json_bytes)
    465
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    466
    467
            // Calculate and write CRC32
    468
    0
            let mut crc = 0u32;
    469
    0
            crc = crc32_update(crc, &Self::APR_MAGIC);
    470
    0
            crc = crc32_update(crc, &len.to_le_bytes());
    471
    0
            crc = crc32_update(crc, json_bytes);
    472
    0
            file.write_all(&crc.to_le_bytes())
    473
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    474
    475
    0
            Ok(())
    476
    0
        }
    477
    478
        /// Load tuner model from .apr file.
    479
    0
        pub fn load_apr<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TunerError> {
    480
            use std::io::Read;
    481
    482
    0
            let mut file =
    483
    0
                std::fs::File::open(path).map_err(|e| TunerError::Io(e.to_string()))?;
    484
    485
            // Read and verify magic
    486
    0
            let mut magic = [0u8; 4];
    487
    0
            file.read_exact(&mut magic)
    488
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    489
    490
    0
            if magic != Self::APR_MAGIC {
    491
    0
                return Err(TunerError::InvalidFormat(
    492
    0
                    "Invalid APR magic bytes".to_string(),
    493
    0
                ));
    494
    0
            }
    495
    496
            // Read metadata length
    497
    0
            let mut len_bytes = [0u8; 4];
    498
    0
            file.read_exact(&mut len_bytes)
    499
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    500
    0
            let len = u32::from_le_bytes(len_bytes) as usize;
    501
    502
            // Read JSON metadata
    503
    0
            let mut json_bytes = vec![0u8; len];
    504
    0
            file.read_exact(&mut json_bytes)
    505
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    506
    507
            // Read and verify CRC32
    508
    0
            let mut crc_bytes = [0u8; 4];
    509
    0
            file.read_exact(&mut crc_bytes)
    510
    0
                .map_err(|e| TunerError::Io(e.to_string()))?;
    511
    0
            let stored_crc = u32::from_le_bytes(crc_bytes);
    512
    513
    0
            let mut computed_crc = 0u32;
    514
    0
            computed_crc = crc32_update(computed_crc, &Self::APR_MAGIC);
    515
    0
            computed_crc = crc32_update(computed_crc, &len_bytes);
    516
    0
            computed_crc = crc32_update(computed_crc, &json_bytes);
    517
    518
    0
            if stored_crc != computed_crc {
    519
    0
                return Err(TunerError::InvalidFormat(
    520
    0
                    "CRC32 checksum mismatch".to_string(),
    521
    0
                ));
    522
    0
            }
    523
    524
            // Parse JSON
    525
    0
            let json = String::from_utf8(json_bytes)
    526
    0
                .map_err(|e| TunerError::Serialization(e.to_string()))?;
    527
    528
    0
            Self::from_json(&json)
    529
    0
        }
    530
    531
        /// Save to default cache path.
    532
        #[cfg(feature = "hardware-detect")]
    533
        pub fn save_to_cache(&self) -> Result<(), TunerError> {
    534
            self.save_apr(Self::cache_path())
    535
        }
    536
    }