/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 | | } |