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/features.rs
Line
Count
Source
1
//! Feature Extraction for ML Tuning
2
//!
3
//! Implements TunerFeatures, TunerFeaturesBuilder, FeatureExtractor, and RunConfig.
4
5
use crate::brick::{BrickCategory, BrickProfiler};
6
use crate::hardware::HardwareCapability;
7
use serde::{Deserialize, Serialize};
8
9
use super::error::TunerError;
10
use super::types::{BottleneckClass, KernelType, QuantType};
11
12
// ============================================================================
13
// TunerFeatures
14
// ============================================================================
15
16
/// Feature vector for ML-based kernel tuning.
17
///
18
/// All fields normalized to [0, 1] for model input.
19
/// Total dimension: 42 features.
20
///
21
/// # Feature Categories
22
///
23
/// - **Static (11)**: Known before execution (model size, batch size, etc.)
24
/// - **Quant one-hot (8)**: Quantization type encoding
25
/// - **Kernel one-hot (16)**: Kernel type encoding
26
/// - **Hardware (5)**: GPU capabilities
27
/// - **Derived (2)**: Computed features (arithmetic intensity, efficiency)
28
#[derive(Debug, Clone, Serialize, Deserialize)]
29
pub struct TunerFeatures {
30
    // === Static features (11) ===
31
    /// Model size in billions (log10 normalized)
32
    pub model_params_b: f32,
33
    /// Hidden dimension / 16384
34
    pub hidden_dim_norm: f32,
35
    /// Number of layers / 128
36
    pub num_layers_norm: f32,
37
    /// Number of attention heads / 128
38
    pub num_heads_norm: f32,
39
    /// Head dimension / 256
40
    pub head_dim_norm: f32,
41
    /// Vocabulary size (log10 normalized)
42
    pub vocab_size_log: f32,
43
    /// Batch size M / 64
44
    pub batch_size_norm: f32,
45
    /// Sequence length (log2 / 15)
46
    pub seq_len_log: f32,
47
    /// CUDA graphs enabled (0 or 1)
48
    pub cuda_graphs: f32,
49
    /// Number of KV caches / batch_size (for multi-cache detection)
50
    pub kv_cache_ratio: f32,
51
    /// Prefill vs decode (0=decode, 1=prefill)
52
    pub is_prefill: f32,
53
54
    // === Quantization one-hot (8) ===
55
    pub quant_type_onehot: [f32; 8],
56
57
    // === Kernel one-hot (16) ===
58
    pub kernel_type_onehot: [f32; 16],
59
60
    // === Hardware features (5) === [v1.1.0: added L2 cache + zero-copy]
61
    /// Memory bandwidth / 3000 GB/s
62
    pub gpu_mem_bw_norm: f32,
63
    /// Compute TFLOPS / 500
64
    pub gpu_compute_norm: f32,
65
    /// SM count / 200
66
    pub gpu_sm_norm: f32,
67
    /// L2 cache size / 128 MB (v1.1.0: critical for occupancy)
68
    pub gpu_l2_cache_norm: f32,
69
    /// Zero-copy memory path enabled (0 or 1) (v1.1.0: pinned memory)
70
    pub is_zero_copy: f32,
71
72
    // === Derived features (2) ===
73
    /// Arithmetic intensity (FLOP/byte), normalized
74
    pub arithmetic_intensity: f32,
75
    /// Theoretical efficiency (measured / roofline)
76
    pub theoretical_efficiency: f32,
77
78
    // === Labels (for training) ===
79
    /// Measured throughput (tokens/second) - training label
80
    #[serde(skip_serializing_if = "Option::is_none")]
81
    pub measured_tps: Option<f32>,
82
    /// Best kernel ID - classification label
83
    #[serde(skip_serializing_if = "Option::is_none")]
84
    pub best_kernel_id: Option<u8>,
85
    /// Bottleneck class - classification label
86
    #[serde(skip_serializing_if = "Option::is_none")]
87
    pub bottleneck_class: Option<BottleneckClass>,
88
}
89
90
impl Default for TunerFeatures {
91
0
    fn default() -> Self {
92
0
        Self {
93
0
            model_params_b: 0.0,
94
0
            hidden_dim_norm: 0.0,
95
0
            num_layers_norm: 0.0,
96
0
            num_heads_norm: 0.0,
97
0
            head_dim_norm: 0.0,
98
0
            vocab_size_log: 0.0,
99
0
            batch_size_norm: 0.0,
100
0
            seq_len_log: 0.0,
101
0
            cuda_graphs: 0.0,
102
0
            kv_cache_ratio: 1.0,
103
0
            is_prefill: 0.0,
104
0
            quant_type_onehot: [0.0; 8],
105
0
            kernel_type_onehot: [0.0; 16],
106
0
            gpu_mem_bw_norm: 0.0,
107
0
            gpu_compute_norm: 0.0,
108
0
            gpu_sm_norm: 0.0,
109
0
            gpu_l2_cache_norm: 0.0,
110
0
            is_zero_copy: 0.0,
111
0
            arithmetic_intensity: 0.0,
112
0
            theoretical_efficiency: 0.0,
113
0
            measured_tps: None,
114
0
            best_kernel_id: None,
115
0
            bottleneck_class: None,
116
0
        }
117
0
    }
118
}
119
120
impl TunerFeatures {
121
    /// Total feature dimension (excluding labels)
122
    /// v1.1.0: 11 static + 8 quant + 16 kernel + 5 hardware + 2 derived = 42
123
    pub const DIM: usize = 11 + 8 + 16 + 5 + 2; // 42 features (v1.1.0)
124
125
    /// Create a new feature builder
126
0
    pub fn builder() -> TunerFeaturesBuilder {
127
0
        TunerFeaturesBuilder::default()
128
0
    }
129
130
    /// Convert to flat vector for model input
131
0
    pub fn to_vector(&self) -> Vec<f32> {
132
0
        let mut v = Vec::with_capacity(Self::DIM);
133
134
        // Static features
135
0
        v.push(self.model_params_b);
136
0
        v.push(self.hidden_dim_norm);
137
0
        v.push(self.num_layers_norm);
138
0
        v.push(self.num_heads_norm);
139
0
        v.push(self.head_dim_norm);
140
0
        v.push(self.vocab_size_log);
141
0
        v.push(self.batch_size_norm);
142
0
        v.push(self.seq_len_log);
143
0
        v.push(self.cuda_graphs);
144
0
        v.push(self.kv_cache_ratio);
145
0
        v.push(self.is_prefill);
146
147
        // One-hot encodings
148
0
        v.extend_from_slice(&self.quant_type_onehot);
149
0
        v.extend_from_slice(&self.kernel_type_onehot);
150
151
        // Hardware features (5) [v1.1.0]
152
0
        v.push(self.gpu_mem_bw_norm);
153
0
        v.push(self.gpu_compute_norm);
154
0
        v.push(self.gpu_sm_norm);
155
0
        v.push(self.gpu_l2_cache_norm); // v1.1.0
156
0
        v.push(self.is_zero_copy); // v1.1.0
157
158
        // Derived features
159
0
        v.push(self.arithmetic_intensity);
160
0
        v.push(self.theoretical_efficiency);
161
162
0
        v
163
0
    }
164
165
    /// Validate features (F021-F030 falsification criteria)
166
0
    pub fn validate(&self) -> Result<(), TunerError> {
167
0
        let v = self.to_vector();
168
169
        // F021: No NaN features
170
0
        if v.iter().any(|x| x.is_nan()) {
171
0
            return Err(TunerError::InvalidFeature("NaN value in features".into()));
172
0
        }
173
174
        // F022: No infinite features
175
0
        if v.iter().any(|x| x.is_infinite()) {
176
0
            return Err(TunerError::InvalidFeature(
177
0
                "Infinite value in features".into(),
178
0
            ));
179
0
        }
180
181
        // F023: All features in [0, 1] (with small tolerance for floating point)
182
0
        if v.iter().any(|x| *x < -0.001 || *x > 1.001) {
183
0
            return Err(TunerError::InvalidFeature(
184
0
                "Feature value outside [0, 1]".into(),
185
0
            ));
186
0
        }
187
188
        // F029: One-hot sums = 1
189
0
        let quant_sum: f32 = self.quant_type_onehot.iter().sum();
190
0
        if (quant_sum - 1.0).abs() > 0.001 && quant_sum > 0.001 {
191
0
            return Err(TunerError::InvalidFeature(
192
0
                "Quant one-hot does not sum to 1".into(),
193
0
            ));
194
0
        }
195
196
0
        let kernel_sum: f32 = self.kernel_type_onehot.iter().sum();
197
0
        if (kernel_sum - 1.0).abs() > 0.001 && kernel_sum > 0.001 {
198
0
            return Err(TunerError::InvalidFeature(
199
0
                "Kernel one-hot does not sum to 1".into(),
200
0
            ));
201
0
        }
202
203
0
        Ok(())
204
0
    }
205
}
206
207
// ============================================================================
208
// TunerFeaturesBuilder
209
// ============================================================================
210
211
/// Builder for TunerFeatures with automatic normalization.
212
#[derive(Default)]
213
pub struct TunerFeaturesBuilder {
214
    model_params_b: Option<f32>,
215
    hidden_dim: Option<u32>,
216
    num_layers: Option<u32>,
217
    num_heads: Option<u32>,
218
    head_dim: Option<u32>,
219
    vocab_size: Option<u32>,
220
    batch_size: Option<u32>,
221
    seq_len: Option<u32>,
222
    cuda_graphs: bool,
223
    kv_caches: Option<u32>,
224
    is_prefill: bool,
225
    quant_type: Option<QuantType>,
226
    kernel_type: Option<KernelType>,
227
    gpu_mem_bw_gbs: Option<f32>,
228
    gpu_compute_tflops: Option<f32>,
229
    gpu_sm_count: Option<u32>,
230
    gpu_l2_cache_mb: Option<f32>, // v1.1.0
231
    is_zero_copy: bool,           // v1.1.0
232
    measured_tps: Option<f32>,
233
}
234
235
impl TunerFeaturesBuilder {
236
    /// Set model size in billions of parameters
237
0
    pub fn model_params_b(mut self, params: f32) -> Self {
238
0
        self.model_params_b = Some(params);
239
0
        self
240
0
    }
241
242
    /// Set hidden dimension
243
0
    pub fn hidden_dim(mut self, dim: u32) -> Self {
244
0
        self.hidden_dim = Some(dim);
245
0
        self
246
0
    }
247
248
    /// Set number of layers
249
0
    pub fn num_layers(mut self, layers: u32) -> Self {
250
0
        self.num_layers = Some(layers);
251
0
        self
252
0
    }
253
254
    /// Set number of attention heads
255
0
    pub fn num_heads(mut self, heads: u32) -> Self {
256
0
        self.num_heads = Some(heads);
257
0
        self
258
0
    }
259
260
    /// Set head dimension
261
0
    pub fn head_dim(mut self, dim: u32) -> Self {
262
0
        self.head_dim = Some(dim);
263
0
        self
264
0
    }
265
266
    /// Set vocabulary size
267
0
    pub fn vocab_size(mut self, size: u32) -> Self {
268
0
        self.vocab_size = Some(size);
269
0
        self
270
0
    }
271
272
    /// Set batch size (M)
273
0
    pub fn batch_size(mut self, m: u32) -> Self {
274
0
        self.batch_size = Some(m);
275
0
        self
276
0
    }
277
278
    /// Set sequence length
279
0
    pub fn seq_len(mut self, len: u32) -> Self {
280
0
        self.seq_len = Some(len);
281
0
        self
282
0
    }
283
284
    /// Enable CUDA graphs
285
0
    pub fn cuda_graphs(mut self, enabled: bool) -> Self {
286
0
        self.cuda_graphs = enabled;
287
0
        self
288
0
    }
289
290
    /// Set number of KV caches
291
0
    pub fn kv_caches(mut self, count: u32) -> Self {
292
0
        self.kv_caches = Some(count);
293
0
        self
294
0
    }
295
296
    /// Set prefill mode
297
0
    pub fn is_prefill(mut self, prefill: bool) -> Self {
298
0
        self.is_prefill = prefill;
299
0
        self
300
0
    }
301
302
    /// Set quantization type
303
0
    pub fn quant_type(mut self, qt: QuantType) -> Self {
304
0
        self.quant_type = Some(qt);
305
0
        self
306
0
    }
307
308
    /// Set kernel type
309
0
    pub fn kernel_type(mut self, kt: KernelType) -> Self {
310
0
        self.kernel_type = Some(kt);
311
0
        self
312
0
    }
313
314
    /// Set GPU memory bandwidth in GB/s
315
0
    pub fn gpu_mem_bw_gbs(mut self, bw: f32) -> Self {
316
0
        self.gpu_mem_bw_gbs = Some(bw);
317
0
        self
318
0
    }
319
320
    /// Set GPU compute in TFLOPS
321
0
    pub fn gpu_compute_tflops(mut self, tflops: f32) -> Self {
322
0
        self.gpu_compute_tflops = Some(tflops);
323
0
        self
324
0
    }
325
326
    /// Set GPU SM count
327
0
    pub fn gpu_sm_count(mut self, count: u32) -> Self {
328
0
        self.gpu_sm_count = Some(count);
329
0
        self
330
0
    }
331
332
    /// Set measured throughput (for training data)
333
0
    pub fn measured_tps(mut self, tps: f32) -> Self {
334
0
        self.measured_tps = Some(tps);
335
0
        self
336
0
    }
337
338
    /// Set L2 cache size in MB (v1.1.0)
339
0
    pub fn gpu_l2_cache_mb(mut self, l2_mb: f32) -> Self {
340
0
        self.gpu_l2_cache_mb = Some(l2_mb);
341
0
        self
342
0
    }
343
344
    /// Set zero-copy memory path enabled (v1.1.0)
345
0
    pub fn is_zero_copy(mut self, enabled: bool) -> Self {
346
0
        self.is_zero_copy = enabled;
347
0
        self
348
0
    }
349
350
    /// Set hardware capability (auto-fills GPU features)
351
0
    pub fn hardware(mut self, hw: &HardwareCapability) -> Self {
352
0
        if let Some(gpu) = &hw.gpu {
353
0
            self.gpu_mem_bw_gbs = Some(gpu.memory_bw_gbps as f32);
354
0
            self.gpu_compute_tflops = Some(gpu.peak_tflops_fp32 as f32);
355
0
            // SM count not directly available; estimate from compute capability
356
0
            self.gpu_sm_count = None;
357
0
        }
358
0
        self
359
0
    }
360
361
    /// Build the feature vector with normalization
362
0
    pub fn build(self) -> TunerFeatures {
363
0
        let batch_size = self.batch_size.unwrap_or(1);
364
0
        let kv_caches = self.kv_caches.unwrap_or(batch_size);
365
366
        // Create one-hot encodings
367
0
        let mut quant_onehot = [0.0f32; 8];
368
0
        if let Some(qt) = self.quant_type {
369
0
            quant_onehot[qt.to_index()] = 1.0;
370
0
        }
371
372
0
        let mut kernel_onehot = [0.0f32; 16];
373
0
        if let Some(kt) = self.kernel_type {
374
0
            kernel_onehot[kt.to_index()] = 1.0;
375
0
        }
376
377
        // Calculate derived features
378
0
        let hidden_dim = self.hidden_dim.unwrap_or(1536) as f32;
379
0
        let batch_size_f = batch_size as f32;
380
0
        let quant_bytes = self
381
0
            .quant_type
382
0
            .map(|q| q.bytes_per_param())
383
0
            .unwrap_or(0.5625);
384
385
        // Arithmetic intensity for GEMV: 2*N*K FLOPs / (N*K*bytes + K + N) bytes
386
        // Simplified: ~2 / bytes_per_param for memory-bound inference
387
0
        let arithmetic_intensity = (2.0 / quant_bytes).min(10.0) / 10.0;
388
389
        // Theoretical efficiency starts at 0 (unknown until measured)
390
0
        let theoretical_efficiency = 0.0;
391
392
        TunerFeatures {
393
            // Normalized static features
394
0
            model_params_b: self
395
0
                .model_params_b
396
0
                .map(|p| (p.log10() + 1.0) / 3.0) // log10(0.1)=-1, log10(100)=2 → [0, 1]
397
0
                .unwrap_or(0.0)
398
0
                .clamp(0.0, 1.0),
399
0
            hidden_dim_norm: (hidden_dim / 16384.0).clamp(0.0, 1.0),
400
0
            num_layers_norm: (self.num_layers.unwrap_or(28) as f32 / 128.0).clamp(0.0, 1.0),
401
0
            num_heads_norm: (self.num_heads.unwrap_or(12) as f32 / 128.0).clamp(0.0, 1.0),
402
0
            head_dim_norm: (self.head_dim.unwrap_or(128) as f32 / 256.0).clamp(0.0, 1.0),
403
0
            vocab_size_log: self
404
0
                .vocab_size
405
0
                .map(|v| (v as f32).log10() / 6.0) // log10(1M)=6
406
0
                .unwrap_or(0.0)
407
0
                .clamp(0.0, 1.0),
408
0
            batch_size_norm: (batch_size_f / 64.0).clamp(0.0, 1.0),
409
0
            seq_len_log: self
410
0
                .seq_len
411
0
                .map(|s| (s as f32).log2() / 15.0) // log2(32K)≈15
412
0
                .unwrap_or(0.0)
413
0
                .clamp(0.0, 1.0),
414
0
            cuda_graphs: if self.cuda_graphs { 1.0 } else { 0.0 },
415
0
            kv_cache_ratio: (kv_caches as f32 / batch_size_f).clamp(0.0, 1.0),
416
0
            is_prefill: if self.is_prefill { 1.0 } else { 0.0 },
417
418
            // One-hot encodings
419
0
            quant_type_onehot: quant_onehot,
420
0
            kernel_type_onehot: kernel_onehot,
421
422
            // Hardware features (5) [v1.1.0]
423
0
            gpu_mem_bw_norm: (self.gpu_mem_bw_gbs.unwrap_or(1000.0) / 3000.0).clamp(0.0, 1.0),
424
0
            gpu_compute_norm: (self.gpu_compute_tflops.unwrap_or(100.0) / 500.0).clamp(0.0, 1.0),
425
0
            gpu_sm_norm: (self.gpu_sm_count.unwrap_or(128) as f32 / 200.0).clamp(0.0, 1.0),
426
0
            gpu_l2_cache_norm: (self.gpu_l2_cache_mb.unwrap_or(48.0) / 128.0).clamp(0.0, 1.0), // v1.1.0
427
0
            is_zero_copy: if self.is_zero_copy { 1.0 } else { 0.0 }, // v1.1.0
428
429
            // Derived features
430
0
            arithmetic_intensity,
431
0
            theoretical_efficiency,
432
433
            // Labels
434
0
            measured_tps: self.measured_tps,
435
0
            best_kernel_id: None,
436
0
            bottleneck_class: None,
437
        }
438
0
    }
439
}
440
441
// ============================================================================
442
// FeatureExtractor
443
// ============================================================================
444
445
/// Extracts features from BrickProfiler and runtime configuration.
446
#[derive(Debug)]
447
pub struct FeatureExtractor {
448
    /// Hardware capability (cached)
449
    pub(crate) hardware: Option<HardwareCapability>,
450
}
451
452
impl Default for FeatureExtractor {
453
0
    fn default() -> Self {
454
0
        Self::new()
455
0
    }
456
}
457
458
impl FeatureExtractor {
459
    /// Create a new feature extractor
460
0
    pub fn new() -> Self {
461
0
        Self { hardware: None }
462
0
    }
463
464
    /// Create with hardware capability
465
0
    pub fn with_hardware(hardware: HardwareCapability) -> Self {
466
0
        Self {
467
0
            hardware: Some(hardware),
468
0
        }
469
0
    }
470
471
    /// Extract features from profiler and configuration
472
0
    pub fn extract(&self, profiler: &BrickProfiler, config: &RunConfig) -> TunerFeatures {
473
0
        let mut builder = TunerFeatures::builder()
474
0
            .model_params_b(config.model_params_b)
475
0
            .hidden_dim(config.hidden_dim)
476
0
            .num_layers(config.num_layers)
477
0
            .num_heads(config.num_heads)
478
0
            .batch_size(config.batch_size)
479
0
            .seq_len(config.seq_len)
480
0
            .cuda_graphs(config.cuda_graphs)
481
0
            .quant_type(config.quant_type)
482
0
            .kernel_type(config.kernel_type);
483
484
        // Add hardware features if available
485
0
        if let Some(hw) = &self.hardware {
486
0
            builder = builder.hardware(hw);
487
0
        }
488
489
        // Add measured throughput if available
490
0
        if let Some(tps) = profiler.tokens_per_sec() {
491
0
            builder = builder.measured_tps(tps);
492
0
        }
493
494
0
        let mut features = builder.build();
495
496
        // Update derived features from profiler
497
0
        if let Some(efficiency) = self.calculate_efficiency(profiler, config) {
498
0
            features.theoretical_efficiency = efficiency;
499
0
        }
500
501
        // Classify bottleneck from profiler data
502
0
        features.bottleneck_class = Some(self.classify_bottleneck(profiler));
503
504
0
        features
505
0
    }
506
507
    /// Calculate efficiency from profiler data
508
0
    pub fn calculate_efficiency(&self, profiler: &BrickProfiler, config: &RunConfig) -> Option<f32> {
509
0
        let measured_tps = profiler.tokens_per_sec()?;
510
0
        let hw = self.hardware.as_ref()?;
511
0
        let gpu = hw.gpu.as_ref()?;
512
513
        // Calculate theoretical max based on roofline
514
0
        let bytes_per_token = config.model_params_b * 1e9 * config.quant_type.bytes_per_param();
515
0
        let theoretical_tps = (gpu.memory_bw_gbps as f32) * 1e9 / bytes_per_token;
516
517
0
        Some((measured_tps / theoretical_tps).clamp(0.0, 1.0))
518
0
    }
519
520
    /// Classify bottleneck from profiler brick breakdown.
521
    ///
522
    /// PAR-200: Uses category_stats() for efficient aggregation.
523
0
    pub fn classify_bottleneck(&self, profiler: &BrickProfiler) -> BottleneckClass {
524
0
        let cats = profiler.category_stats();
525
0
        let total_ns = profiler.total_ns();
526
527
0
        if total_ns == 0 {
528
0
            return BottleneckClass::Unknown;
529
0
        }
530
531
        // Get category percentages
532
0
        let attention_pct =
533
0
            cats[BrickCategory::Attention as usize].percentage(total_ns) as f32 / 100.0;
534
0
        let ffn_pct = cats[BrickCategory::Ffn as usize].percentage(total_ns) as f32 / 100.0;
535
0
        let norm_pct = cats[BrickCategory::Norm as usize].percentage(total_ns) as f32 / 100.0;
536
537
        // Classify based on dominant component
538
0
        if attention_pct > 0.35 {
539
0
            BottleneckClass::AttentionBound
540
0
        } else if ffn_pct > 0.50 {
541
            // FFN is memory-bound (large GEMV operations)
542
0
            BottleneckClass::MemoryBound
543
0
        } else if norm_pct > 0.20 {
544
            // High norm percentage indicates launch overhead
545
0
            BottleneckClass::LaunchBound
546
        } else {
547
0
            BottleneckClass::MemoryBound // Default for inference
548
        }
549
0
    }
550
}
551
552
// ============================================================================
553
// RunConfig
554
// ============================================================================
555
556
/// Runtime configuration for feature extraction
557
#[derive(Debug, Clone, Serialize, Deserialize)]
558
pub struct RunConfig {
559
    pub model_params_b: f32,
560
    pub hidden_dim: u32,
561
    pub num_layers: u32,
562
    pub num_heads: u32,
563
    pub batch_size: u32,
564
    pub seq_len: u32,
565
    pub cuda_graphs: bool,
566
    pub quant_type: QuantType,
567
    pub kernel_type: KernelType,
568
}
569
570
impl Default for RunConfig {
571
0
    fn default() -> Self {
572
0
        Self {
573
0
            model_params_b: 1.5,
574
0
            hidden_dim: 1536,
575
0
            num_layers: 28,
576
0
            num_heads: 12,
577
0
            batch_size: 1,
578
0
            seq_len: 1,
579
0
            cuda_graphs: false,
580
0
            quant_type: QuantType::Q4K,
581
0
            kernel_type: KernelType::VectorizedQ4K,
582
0
        }
583
0
    }
584
}