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/blis/profiler.rs
Line
Count
Source
1
//! BLIS Profiler Integration
2
//!
3
//! Performance tracking for BLIS operations at multiple granularity levels.
4
//! Supports Kaizen (continuous improvement) methodology.
5
//!
6
//! # Philosophy
7
//!
8
//! Kaizen (改善) means "continuous improvement." By tracking performance metrics
9
//! at each level of the BLIS hierarchy, we can identify bottlenecks and measure
10
//! the impact of optimizations.
11
//!
12
//! # Profiling Levels
13
//!
14
//! - **Macro**: L3 cache blocking level (NC x KC tiles)
15
//! - **Midi**: L2 cache blocking level (MC x KC tiles)
16
//! - **Micro**: Microkernel level (MR x NR tiles)
17
//! - **Pack**: Data packing operations
18
//!
19
//! # Usage
20
//!
21
//! ```
22
//! use trueno::blis::profiler::{BlisProfiler, BlisProfileLevel};
23
//!
24
//! let mut profiler = BlisProfiler::enabled();
25
//! profiler.record(BlisProfileLevel::Micro, 1000, 384);
26
//! println!("{}", profiler.summary());
27
//! ```
28
29
// ============================================================================
30
// Kaizen (Continuous Improvement) - Performance Tracking
31
// ============================================================================
32
33
/// Kaizen metrics for tracking improvement
34
#[derive(Debug, Clone, Default)]
35
pub struct KaizenMetrics {
36
    /// Total FLOP count
37
    pub flops: u64,
38
    /// Total time in nanoseconds
39
    pub time_ns: u64,
40
    /// Number of measurements
41
    pub samples: usize,
42
}
43
44
impl KaizenMetrics {
45
    /// Record a GEMM operation
46
0
    pub fn record(&mut self, m: usize, n: usize, k: usize, duration: std::time::Duration) {
47
0
        self.flops += 2 * m as u64 * n as u64 * k as u64;
48
0
        self.time_ns += duration.as_nanos() as u64;
49
0
        self.samples += 1;
50
0
    }
51
52
    /// Get achieved GFLOP/s
53
0
    pub fn gflops(&self) -> f64 {
54
0
        if self.time_ns == 0 {
55
0
            return 0.0;
56
0
        }
57
0
        self.flops as f64 / self.time_ns as f64
58
0
    }
59
60
    /// Reset metrics
61
0
    pub fn reset(&mut self) {
62
0
        *self = Self::default();
63
0
    }
64
}
65
66
// ============================================================================
67
// BLIS Profiler Integration
68
// ============================================================================
69
70
/// Profiling level for BLIS operations
71
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72
pub enum BlisProfileLevel {
73
    /// L3 block level (NC x KC tiles)
74
    Macro,
75
    /// L2 block level (MC x KC tiles)
76
    Midi,
77
    /// Microkernel level (MR x NR tiles)
78
    Micro,
79
    /// Packing operations
80
    Pack,
81
}
82
83
/// Statistics for a profiling level
84
#[derive(Debug, Clone, Default)]
85
pub struct BlisLevelStats {
86
    /// Total time in nanoseconds
87
    pub total_ns: u64,
88
    /// Number of invocations
89
    pub count: u64,
90
    /// Total FLOPs at this level
91
    pub flops: u64,
92
}
93
94
impl BlisLevelStats {
95
    /// Record a timing
96
0
    pub fn record(&mut self, duration_ns: u64, flops: u64) {
97
0
        self.total_ns += duration_ns;
98
0
        self.count += 1;
99
0
        self.flops += flops;
100
0
    }
101
102
    /// Get average time in microseconds
103
0
    pub fn avg_us(&self) -> f64 {
104
0
        if self.count == 0 {
105
0
            return 0.0;
106
0
        }
107
0
        self.total_ns as f64 / self.count as f64 / 1000.0
108
0
    }
109
110
    /// Get GFLOP/s
111
0
    pub fn gflops(&self) -> f64 {
112
0
        if self.total_ns == 0 {
113
0
            return 0.0;
114
0
        }
115
0
        self.flops as f64 / self.total_ns as f64
116
0
    }
117
}
118
119
/// BLIS-aware profiler
120
#[derive(Debug, Clone, Default)]
121
pub struct BlisProfiler {
122
    /// Per-level statistics
123
    pub macro_stats: BlisLevelStats,
124
    pub midi_stats: BlisLevelStats,
125
    pub micro_stats: BlisLevelStats,
126
    pub pack_stats: BlisLevelStats,
127
    /// Whether profiling is enabled
128
    pub enabled: bool,
129
}
130
131
impl BlisProfiler {
132
    /// Create a new profiler (disabled by default)
133
0
    pub fn new() -> Self {
134
0
        Self::default()
135
0
    }
136
137
    /// Create an enabled profiler
138
0
    pub fn enabled() -> Self {
139
0
        Self {
140
0
            enabled: true,
141
0
            ..Self::default()
142
0
        }
143
0
    }
144
145
    /// Record timing for a level
146
0
    pub fn record(&mut self, level: BlisProfileLevel, duration_ns: u64, flops: u64) {
147
0
        if !self.enabled {
148
0
            return;
149
0
        }
150
0
        match level {
151
0
            BlisProfileLevel::Macro => self.macro_stats.record(duration_ns, flops),
152
0
            BlisProfileLevel::Midi => self.midi_stats.record(duration_ns, flops),
153
0
            BlisProfileLevel::Micro => self.micro_stats.record(duration_ns, flops),
154
0
            BlisProfileLevel::Pack => self.pack_stats.record(duration_ns, 0),
155
        }
156
0
    }
157
158
    /// Get total GFLOP/s
159
0
    pub fn total_gflops(&self) -> f64 {
160
0
        let total_ns = self.macro_stats.total_ns;
161
0
        let total_flops = self.macro_stats.flops;
162
0
        if total_ns == 0 {
163
0
            return 0.0;
164
0
        }
165
0
        total_flops as f64 / total_ns as f64
166
0
    }
167
168
    /// Generate summary report
169
0
    pub fn summary(&self) -> String {
170
0
        let mut s = String::new();
171
0
        s.push_str("BLIS Profiler Summary\n");
172
0
        s.push_str("=====================\n");
173
0
        s.push_str(&format!(
174
0
            "Macro: {:.1}us avg, {:.1} GFLOP/s, {} calls\n",
175
0
            self.macro_stats.avg_us(),
176
0
            self.macro_stats.gflops(),
177
0
            self.macro_stats.count
178
0
        ));
179
0
        s.push_str(&format!(
180
0
            "Midi:  {:.1}us avg, {:.1} GFLOP/s, {} calls\n",
181
0
            self.midi_stats.avg_us(),
182
0
            self.midi_stats.gflops(),
183
0
            self.midi_stats.count
184
0
        ));
185
0
        s.push_str(&format!(
186
0
            "Micro: {:.1}us avg, {:.1} GFLOP/s, {} calls\n",
187
0
            self.micro_stats.avg_us(),
188
0
            self.micro_stats.gflops(),
189
0
            self.micro_stats.count
190
0
        ));
191
0
        s.push_str(&format!(
192
0
            "Pack:  {:.1}us avg, {} calls\n",
193
0
            self.pack_stats.avg_us(),
194
0
            self.pack_stats.count
195
0
        ));
196
0
        s.push_str(&format!("Total: {:.1} GFLOP/s\n", self.total_gflops()));
197
0
        s
198
0
    }
199
200
    /// Reset all statistics
201
0
    pub fn reset(&mut self) {
202
0
        self.macro_stats = BlisLevelStats::default();
203
0
        self.midi_stats = BlisLevelStats::default();
204
0
        self.micro_stats = BlisLevelStats::default();
205
0
        self.pack_stats = BlisLevelStats::default();
206
0
    }
207
}
208
209
#[cfg(test)]
210
mod tests {
211
    use super::*;
212
    use std::time::Duration;
213
214
    #[test]
215
    fn test_kaizen_metrics_default() {
216
        let m = KaizenMetrics::default();
217
        assert_eq!(m.flops, 0);
218
        assert_eq!(m.time_ns, 0);
219
        assert_eq!(m.samples, 0);
220
    }
221
222
    #[test]
223
    fn test_kaizen_metrics_record() {
224
        let mut m = KaizenMetrics::default();
225
        m.record(2, 3, 4, Duration::from_nanos(100));
226
        assert_eq!(m.flops, 48); // 2 * 2 * 3 * 4
227
        assert_eq!(m.time_ns, 100);
228
        assert_eq!(m.samples, 1);
229
    }
230
231
    #[test]
232
    fn test_kaizen_metrics_gflops() {
233
        let mut m = KaizenMetrics::default();
234
        m.flops = 1_000_000_000;
235
        m.time_ns = 1_000_000_000;
236
        assert!((m.gflops() - 1.0).abs() < 1e-10);
237
    }
238
239
    #[test]
240
    fn test_kaizen_metrics_gflops_zero_time() {
241
        let m = KaizenMetrics::default();
242
        assert!((m.gflops() - 0.0).abs() < 1e-10);
243
    }
244
245
    #[test]
246
    fn test_kaizen_metrics_reset() {
247
        let mut m = KaizenMetrics::default();
248
        m.record(2, 3, 4, Duration::from_nanos(100));
249
        m.reset();
250
        assert_eq!(m.flops, 0);
251
        assert_eq!(m.time_ns, 0);
252
        assert_eq!(m.samples, 0);
253
    }
254
255
    #[test]
256
    fn test_blis_level_stats_default() {
257
        let s = BlisLevelStats::default();
258
        assert_eq!(s.total_ns, 0);
259
        assert_eq!(s.count, 0);
260
        assert_eq!(s.flops, 0);
261
    }
262
263
    #[test]
264
    fn test_blis_level_stats_record() {
265
        let mut s = BlisLevelStats::default();
266
        s.record(1000, 500);
267
        assert_eq!(s.total_ns, 1000);
268
        assert_eq!(s.count, 1);
269
        assert_eq!(s.flops, 500);
270
    }
271
272
    #[test]
273
    fn test_blis_level_stats_avg_us() {
274
        let mut s = BlisLevelStats::default();
275
        s.record(2000, 0);
276
        s.record(4000, 0);
277
        assert!((s.avg_us() - 3.0).abs() < 1e-10);
278
    }
279
280
    #[test]
281
    fn test_blis_level_stats_avg_us_zero_count() {
282
        let s = BlisLevelStats::default();
283
        assert!((s.avg_us() - 0.0).abs() < 1e-10);
284
    }
285
286
    #[test]
287
    fn test_blis_level_stats_gflops() {
288
        let mut s = BlisLevelStats::default();
289
        s.total_ns = 1_000_000_000;
290
        s.flops = 1_000_000_000;
291
        assert!((s.gflops() - 1.0).abs() < 1e-10);
292
    }
293
294
    #[test]
295
    fn test_blis_level_stats_gflops_zero_time() {
296
        let s = BlisLevelStats::default();
297
        assert!((s.gflops() - 0.0).abs() < 1e-10);
298
    }
299
300
    #[test]
301
    fn test_blis_profiler_new() {
302
        let p = BlisProfiler::new();
303
        assert!(!p.enabled);
304
    }
305
306
    #[test]
307
    fn test_blis_profiler_enabled() {
308
        let p = BlisProfiler::enabled();
309
        assert!(p.enabled);
310
    }
311
312
    #[test]
313
    fn test_blis_profiler_record_disabled() {
314
        let mut p = BlisProfiler::new();
315
        p.record(BlisProfileLevel::Micro, 1000, 500);
316
        assert_eq!(p.micro_stats.count, 0);
317
    }
318
319
    #[test]
320
    fn test_blis_profiler_record_enabled() {
321
        let mut p = BlisProfiler::enabled();
322
        p.record(BlisProfileLevel::Micro, 1000, 500);
323
        assert_eq!(p.micro_stats.count, 1);
324
        assert_eq!(p.micro_stats.total_ns, 1000);
325
        assert_eq!(p.micro_stats.flops, 500);
326
    }
327
328
    #[test]
329
    fn test_blis_profiler_record_all_levels() {
330
        let mut p = BlisProfiler::enabled();
331
        p.record(BlisProfileLevel::Macro, 1000, 100);
332
        p.record(BlisProfileLevel::Midi, 2000, 200);
333
        p.record(BlisProfileLevel::Micro, 3000, 300);
334
        p.record(BlisProfileLevel::Pack, 4000, 400);
335
336
        assert_eq!(p.macro_stats.count, 1);
337
        assert_eq!(p.midi_stats.count, 1);
338
        assert_eq!(p.micro_stats.count, 1);
339
        assert_eq!(p.pack_stats.count, 1);
340
        assert_eq!(p.pack_stats.flops, 0); // Pack doesn't track flops
341
    }
342
343
    #[test]
344
    fn test_blis_profiler_total_gflops() {
345
        let mut p = BlisProfiler::enabled();
346
        p.macro_stats.total_ns = 1_000_000_000;
347
        p.macro_stats.flops = 1_000_000_000;
348
        assert!((p.total_gflops() - 1.0).abs() < 1e-10);
349
    }
350
351
    #[test]
352
    fn test_blis_profiler_total_gflops_zero_time() {
353
        let p = BlisProfiler::enabled();
354
        assert!((p.total_gflops() - 0.0).abs() < 1e-10);
355
    }
356
357
    #[test]
358
    fn test_blis_profiler_summary() {
359
        let p = BlisProfiler::enabled();
360
        let summary = p.summary();
361
        assert!(summary.contains("BLIS Profiler Summary"));
362
        assert!(summary.contains("Macro:"));
363
        assert!(summary.contains("Midi:"));
364
        assert!(summary.contains("Micro:"));
365
        assert!(summary.contains("Pack:"));
366
        assert!(summary.contains("Total:"));
367
    }
368
369
    #[test]
370
    fn test_blis_profiler_reset() {
371
        let mut p = BlisProfiler::enabled();
372
        p.record(BlisProfileLevel::Micro, 1000, 500);
373
        p.reset();
374
        assert_eq!(p.micro_stats.count, 0);
375
    }
376
377
    #[test]
378
    fn test_blis_profile_level_debug() {
379
        assert_eq!(format!("{:?}", BlisProfileLevel::Macro), "Macro");
380
        assert_eq!(format!("{:?}", BlisProfileLevel::Midi), "Midi");
381
        assert_eq!(format!("{:?}", BlisProfileLevel::Micro), "Micro");
382
        assert_eq!(format!("{:?}", BlisProfileLevel::Pack), "Pack");
383
    }
384
385
    #[test]
386
    fn test_blis_profile_level_eq() {
387
        assert_eq!(BlisProfileLevel::Macro, BlisProfileLevel::Macro);
388
        assert_ne!(BlisProfileLevel::Macro, BlisProfileLevel::Micro);
389
    }
390
391
    #[test]
392
    fn test_blis_profile_level_hash() {
393
        use std::collections::HashSet;
394
        let mut set = HashSet::new();
395
        set.insert(BlisProfileLevel::Macro);
396
        set.insert(BlisProfileLevel::Micro);
397
        assert_eq!(set.len(), 2);
398
        assert!(set.contains(&BlisProfileLevel::Macro));
399
    }
400
}