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