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
}