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/brick/profiler/mod.rs
Line
Count
Source
1
//! BrickProfiler: Token-Centric Profiling System
2
//!
3
//! TILING-SPEC-001: Tile-Level Profiling Support
4
//!
5
//! This module provides hierarchical profiling for compute bricks:
6
//! - Per-brick timing and throughput (PAR-073)
7
//! - O(1) hot path with BrickId enum (PAR-200)
8
//! - Tile-level profiling for cache-blocked operations (TILING-SPEC-001)
9
//! - Kernel checksum capture for divergence detection (CORRECTNESS-011)
10
11
mod tile_stats;
12
mod checksum;
13
14
#[cfg(test)]
15
mod tests;
16
17
pub use tile_stats::{TileStats, TileLevel, TileTimer};
18
pub use checksum::{KernelChecksum, DivergenceInfo, fnv1a_f32_checksum};
19
20
use std::time::Instant;
21
22
use super::exec_graph::{
23
    BrickBottleneck, BrickCategory, BrickId, BrickStats, CategoryStats, ExecutionGraph,
24
    ExecutionNode, ExecutionNodeId, SyncMode,
25
};
26
27
/// Pending measurement for deferred sync mode.
28
#[derive(Debug, Clone)]
29
struct PendingMeasurement {
30
    /// Brick ID (if known)
31
    brick_id: Option<BrickId>,
32
    /// Brick name (for dynamic bricks)
33
    name: Option<String>,
34
    /// Start time in nanoseconds (from Instant::now())
35
    start_ns: u64,
36
    /// Number of elements processed
37
    elements: u64,
38
}
39
40
/// Per-brick profiler using pure Rust timing.
41
///
42
/// # Design (PAR-073, PAR-200)
43
///
44
/// - Uses `std::time::Instant` for timing (no CUDA event FFI)
45
/// - PAR-200: O(1) hot path with `BrickId` enum + array storage
46
/// - GPU operations require explicit sync before timing point
47
/// - Supports deferred sync mode for low-overhead production profiling
48
/// - Aggregates statistics per brick name
49
///
50
/// # Usage
51
///
52
/// ```rust,ignore
53
/// use trueno::brick::{BrickProfiler, BrickId, SyncMode};
54
///
55
/// let mut profiler = BrickProfiler::new();
56
/// profiler.enable();
57
///
58
/// // Fast path: use BrickId for known bricks (PAR-200)
59
/// let timer = profiler.start_brick(BrickId::RmsNorm);
60
/// // ... do work ...
61
/// // For GPU: cuda_stream.synchronize() HERE
62
/// profiler.stop_brick(timer, 1);
63
///
64
/// // Legacy path: string-based (slower, for unknown bricks)
65
/// let timer = profiler.start("CustomBrick");
66
/// profiler.stop(timer, 1);
67
///
68
/// // Deferred sync mode (production)
69
/// profiler.set_sync_mode(SyncMode::Deferred);
70
/// profiler.record_deferred(BrickId::RmsNorm, start_ns, 1);
71
/// // ... more operations ...
72
/// cuda_stream.synchronize();
73
/// profiler.finalize(end_ns);
74
///
75
/// // Get statistics
76
/// let stats = profiler.brick_stats(BrickId::RmsNorm);
77
/// println!("RmsNorm avg: {:.2}µs", stats.avg_us());
78
///
79
/// // Get category breakdown
80
/// let cats = profiler.category_stats();
81
/// println!("Attention: {:.1}%", cats[BrickCategory::Attention as usize].percentage(profiler.total_ns()));
82
/// ```
83
#[derive(Debug)]
84
pub struct BrickProfiler {
85
    // PAR-200: Fast path - pre-allocated array for known bricks
86
    /// Per-brick statistics for known BrickId types (O(1) lookup)
87
    brick_stats: [BrickStats; BrickId::COUNT],
88
89
    // Legacy path - HashMap for dynamic/unknown brick names
90
    /// Per-brick statistics for unknown brick names (slower, O(1) amortized)
91
    dynamic_stats: std::collections::HashMap<String, BrickStats>,
92
93
    // PAR-200: Deferred sync support
94
    /// Pending measurements awaiting GPU sync
95
    pending: Vec<PendingMeasurement>,
96
    /// Synchronization mode
97
    sync_mode: SyncMode,
98
    /// Reference instant for deferred timing
99
    epoch: Instant,
100
101
    /// Whether profiling is enabled
102
    enabled: bool,
103
    /// Total tokens processed
104
    total_tokens: u64,
105
    /// Total time (ns) across all bricks
106
    total_ns: u64,
107
    /// L2 cache hit rate (0.0-1.0) - v1.1.0 OBSERVE phase
108
    l2_cache_hit_rate: Option<f32>,
109
    /// Whether zero-copy memory transfers are enabled - v1.1.0 OBSERVE phase
110
    is_zero_copy: bool,
111
    /// CORRECTNESS-011: Per-kernel checksums for divergence detection
112
    kernel_checksums: Vec<KernelChecksum>,
113
114
    // PAR-201: Execution path graph
115
    /// Whether execution graph tracking is enabled
116
    graph_enabled: bool,
117
    /// Execution path graph for PTX→kernel→brick relationships
118
    execution_graph: ExecutionGraph,
119
120
    // TILING-SPEC-001: Tile-level profiling
121
    /// Per-level tile statistics (Macro, Midi, Micro)
122
    tile_stats: [TileStats; 3],
123
    /// Whether tile profiling is enabled
124
    tile_profiling_enabled: bool,
125
}
126
127
/// Timer handle returned by `start()` (legacy string-based API).
128
#[derive(Debug)]
129
pub struct BrickTimer {
130
    /// Brick name
131
    name: String,
132
    /// Start time
133
    start: Instant,
134
}
135
136
/// Timer handle returned by `start_brick()` (PAR-200 fast path).
137
#[derive(Debug)]
138
pub struct BrickIdTimer {
139
    /// Brick ID
140
    brick_id: BrickId,
141
    /// Start time
142
    start: Instant,
143
}
144
145
impl Default for BrickProfiler {
146
0
    fn default() -> Self {
147
0
        Self::new()
148
0
    }
149
}
150
151
impl BrickProfiler {
152
    /// Create a new profiler (disabled by default for zero overhead).
153
0
    pub fn new() -> Self {
154
        Self {
155
0
            brick_stats: std::array::from_fn(|i| {
156
                // Safety: i < BrickId::COUNT by construction
157
0
                let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
158
0
                BrickStats::new(brick_id.name())
159
0
            }),
160
0
            dynamic_stats: std::collections::HashMap::new(),
161
0
            pending: Vec::new(),
162
0
            sync_mode: SyncMode::Deferred,
163
0
            epoch: Instant::now(),
164
            enabled: false,
165
            total_tokens: 0,
166
            total_ns: 0,
167
0
            l2_cache_hit_rate: None,
168
            is_zero_copy: false,
169
0
            kernel_checksums: Vec::new(),
170
            graph_enabled: false,
171
0
            execution_graph: ExecutionGraph::new(),
172
0
            tile_stats: [
173
0
                TileStats::new(TileLevel::Macro),
174
0
                TileStats::new(TileLevel::Midi),
175
0
                TileStats::new(TileLevel::Micro),
176
0
            ],
177
            tile_profiling_enabled: false,
178
        }
179
0
    }
180
181
    /// Create an enabled profiler.
182
0
    pub fn enabled() -> Self {
183
0
        let mut profiler = Self::new();
184
0
        profiler.enabled = true;
185
0
        profiler
186
0
    }
187
188
    // ========================================================================
189
    // PAR-200: Sync Mode Configuration
190
    // ========================================================================
191
192
    /// Set the synchronization mode for GPU profiling.
193
    ///
194
    /// # Modes
195
    /// - `Immediate`: Sync after each kernel (accurate but slow)
196
    /// - `PerLayer`: Sync once per transformer layer
197
    /// - `Deferred`: Sync once per forward pass (default, fast)
198
    /// - `None`: No synchronization
199
0
    pub fn set_sync_mode(&mut self, mode: SyncMode) {
200
0
        self.sync_mode = mode;
201
0
    }
202
203
    /// Get the current synchronization mode.
204
    #[must_use]
205
0
    pub fn sync_mode(&self) -> SyncMode {
206
0
        self.sync_mode
207
0
    }
208
209
    /// Reset the epoch for deferred timing.
210
    /// Call this at the start of a forward pass.
211
0
    pub fn reset_epoch(&mut self) {
212
0
        self.epoch = Instant::now();
213
0
    }
214
215
    /// Get nanoseconds elapsed since epoch.
216
    #[inline]
217
0
    pub fn elapsed_ns(&self) -> u64 {
218
0
        self.epoch.elapsed().as_nanos() as u64
219
0
    }
220
221
    // ========================================================================
222
    // PAR-200: Fast Path API (BrickId-based)
223
    // ========================================================================
224
225
    /// Start timing a brick using BrickId (O(1) hot path).
226
    ///
227
    /// This is the preferred API for known brick types.
228
    /// For GPU operations, call `stream.synchronize()` before `stop_brick()`.
229
    #[inline]
230
    #[must_use]
231
0
    pub fn start_brick(&self, brick_id: BrickId) -> BrickIdTimer {
232
0
        BrickIdTimer {
233
0
            brick_id,
234
0
            start: Instant::now(),
235
0
        }
236
0
    }
237
238
    /// Stop timing and record the sample (O(1) hot path).
239
    #[inline]
240
0
    pub fn stop_brick(&mut self, timer: BrickIdTimer, elements: u64) {
241
0
        if !self.enabled {
242
0
            return;
243
0
        }
244
245
0
        let elapsed = timer.start.elapsed();
246
0
        let elapsed_ns = elapsed.as_nanos() as u64;
247
248
        // O(1) array access
249
0
        let stats = &mut self.brick_stats[timer.brick_id as usize];
250
0
        stats.add_sample(elapsed_ns, elements);
251
252
        // Update totals
253
0
        self.total_tokens += elements;
254
0
        self.total_ns += elapsed_ns;
255
0
    }
256
257
    /// Get statistics for a known brick type (O(1)).
258
    #[inline]
259
    #[must_use]
260
0
    pub fn brick_stats(&self, brick_id: BrickId) -> &BrickStats {
261
0
        &self.brick_stats[brick_id as usize]
262
0
    }
263
264
    /// Get mutable statistics for a known brick type (O(1)).
265
    #[inline]
266
0
    pub fn brick_stats_mut(&mut self, brick_id: BrickId) -> &mut BrickStats {
267
0
        &mut self.brick_stats[brick_id as usize]
268
0
    }
269
270
    // ========================================================================
271
    // PAR-200: Deferred Sync API
272
    // ========================================================================
273
274
    /// Record a measurement without GPU sync (deferred mode).
275
    ///
276
    /// Call `finalize()` after GPU sync to apply all pending measurements.
277
    ///
278
    /// # Arguments
279
    /// - `brick_id`: The brick type
280
    /// - `start_ns`: Start time (from `elapsed_ns()` at operation start)
281
    /// - `elements`: Number of elements processed
282
    #[inline]
283
0
    pub fn record_deferred(&mut self, brick_id: BrickId, start_ns: u64, elements: u64) {
284
0
        if !self.enabled {
285
0
            return;
286
0
        }
287
0
        self.pending.push(PendingMeasurement {
288
0
            brick_id: Some(brick_id),
289
0
            name: None,
290
0
            start_ns,
291
0
            elements,
292
0
        });
293
0
    }
294
295
    /// Record a measurement for a dynamic brick (deferred mode).
296
    #[inline]
297
0
    pub fn record_deferred_dynamic(&mut self, name: &str, start_ns: u64, elements: u64) {
298
0
        if !self.enabled {
299
0
            return;
300
0
        }
301
0
        self.pending.push(PendingMeasurement {
302
0
            brick_id: BrickId::from_str(name),
303
0
            name: Some(name.to_string()),
304
0
            start_ns,
305
0
            elements,
306
0
        });
307
0
    }
308
309
    /// Finalize all pending measurements after GPU sync.
310
    ///
311
    /// Must be called after `stream.synchronize()` to get accurate timing.
312
    ///
313
    /// # Arguments
314
    /// - `end_ns`: End time (from `elapsed_ns()` after sync)
315
0
    pub fn finalize(&mut self, end_ns: u64) {
316
0
        if self.pending.is_empty() {
317
0
            return;
318
0
        }
319
320
        // Calculate elapsed time for each pending measurement
321
0
        for m in self.pending.drain(..) {
322
0
            let elapsed_ns = end_ns.saturating_sub(m.start_ns);
323
324
0
            if let Some(brick_id) = m.brick_id {
325
0
                // Fast path: known brick
326
0
                let stats = &mut self.brick_stats[brick_id as usize];
327
0
                stats.add_sample(elapsed_ns, m.elements);
328
0
            } else if let Some(name) = m.name {
329
                // Slow path: dynamic brick
330
0
                let stats = self
331
0
                    .dynamic_stats
332
0
                    .entry(name.clone())
333
0
                    .or_insert_with(|| BrickStats::new(&name));
334
0
                stats.add_sample(elapsed_ns, m.elements);
335
0
            }
336
337
0
            self.total_tokens += m.elements;
338
0
            self.total_ns += elapsed_ns;
339
        }
340
0
    }
341
342
    /// Check if there are pending measurements.
343
    #[inline]
344
    #[must_use]
345
0
    pub fn has_pending(&self) -> bool {
346
0
        !self.pending.is_empty()
347
0
    }
348
349
    /// Get number of pending measurements.
350
    #[inline]
351
    #[must_use]
352
0
    pub fn pending_count(&self) -> usize {
353
0
        self.pending.len()
354
0
    }
355
356
    // ========================================================================
357
    // PAR-200: Category Aggregation
358
    // ========================================================================
359
360
    /// Get aggregated statistics by category.
361
    ///
362
    /// Returns an array indexed by `BrickCategory as usize`.
363
    #[must_use]
364
0
    pub fn category_stats(&self) -> [CategoryStats; BrickCategory::COUNT] {
365
0
        let mut result = [CategoryStats::default(); BrickCategory::COUNT];
366
367
0
        for (i, stats) in self.brick_stats.iter().enumerate() {
368
0
            // Safety: i < BrickId::COUNT by construction
369
0
            let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
370
0
            let cat = brick_id.category() as usize;
371
0
            result[cat].total_ns += stats.total_ns;
372
0
            result[cat].total_elements += stats.total_elements;
373
0
            result[cat].count += stats.count;
374
0
        }
375
376
        // Include dynamic stats in "Other" category
377
0
        for stats in self.dynamic_stats.values() {
378
0
            let cat = BrickCategory::Other as usize;
379
0
            result[cat].total_ns += stats.total_ns;
380
0
            result[cat].total_elements += stats.total_elements;
381
0
            result[cat].count += stats.count;
382
0
        }
383
384
0
        result
385
0
    }
386
387
    /// Print category breakdown to console.
388
0
    pub fn print_category_stats(&self) {
389
0
        let cats = self.category_stats();
390
0
        let total = self.total_ns;
391
392
0
        println!("╭─────────────────────────────────────────────────────────╮");
393
0
        println!("│            Category Breakdown (PAR-200)                 │");
394
0
        println!("├─────────────────────────────────────────────────────────┤");
395
0
        for (i, cat_stats) in cats.iter().enumerate() {
396
            // Safety: i < BrickCategory::COUNT
397
0
            let cat = unsafe { std::mem::transmute::<u8, BrickCategory>(i as u8) };
398
0
            if cat_stats.count > 0 {
399
0
                println!(
400
0
                    "│ {:12} {:8.2}µs avg {:6.1}% [{:5} samples]        │",
401
0
                    cat.name(),
402
0
                    cat_stats.avg_us(),
403
0
                    cat_stats.percentage(total),
404
0
                    cat_stats.count
405
0
                );
406
0
            }
407
        }
408
0
        println!("╰─────────────────────────────────────────────────────────╯");
409
0
    }
410
411
    // ========================================================================
412
    // PAR-201: Execution Path Graph
413
    // ========================================================================
414
415
    /// Enable execution graph tracking.
416
    ///
417
    /// When enabled, the profiler records the execution hierarchy:
418
    /// - Layer → Brick → Kernel relationships
419
    /// - PTX hashes for kernel identity
420
    /// - Timing data per node
421
0
    pub fn enable_graph(&mut self) {
422
0
        self.graph_enabled = true;
423
0
    }
424
425
    /// Disable execution graph tracking.
426
0
    pub fn disable_graph(&mut self) {
427
0
        self.graph_enabled = false;
428
0
    }
429
430
    /// Check if execution graph tracking is enabled.
431
    #[must_use]
432
0
    pub fn is_graph_enabled(&self) -> bool {
433
0
        self.graph_enabled
434
0
    }
435
436
    /// Get the execution graph (immutable).
437
    #[must_use]
438
0
    pub fn execution_graph(&self) -> &ExecutionGraph {
439
0
        &self.execution_graph
440
0
    }
441
442
    /// Get the execution graph (mutable).
443
0
    pub fn execution_graph_mut(&mut self) -> &mut ExecutionGraph {
444
0
        &mut self.execution_graph
445
0
    }
446
447
    /// Push a scope for hierarchical graph recording.
448
    ///
449
    /// # Example
450
    ///
451
    /// ```rust,ignore
452
    /// profiler.enable_graph();
453
    /// profiler.graph_push_scope(ExecutionNode::Layer { index: 0 });
454
    /// // ... record bricks and kernels ...
455
    /// profiler.graph_pop_scope();
456
    /// ```
457
0
    pub fn graph_push_scope(&mut self, node: ExecutionNode) -> Option<ExecutionNodeId> {
458
0
        if !self.graph_enabled {
459
0
            return None;
460
0
        }
461
0
        Some(self.execution_graph.push_scope(node))
462
0
    }
463
464
    /// Pop the current scope.
465
0
    pub fn graph_pop_scope(&mut self) -> Option<ExecutionNodeId> {
466
0
        if !self.graph_enabled {
467
0
            return None;
468
0
        }
469
0
        self.execution_graph.pop_scope()
470
0
    }
471
472
    /// Record a brick in the execution graph.
473
    ///
474
    /// This should be called after `stop_brick()` with the timing data.
475
0
    pub fn graph_record_brick(
476
0
        &mut self,
477
0
        brick_id: BrickId,
478
0
        timing_ns: u64,
479
0
        elements: u64,
480
0
    ) -> Option<ExecutionNodeId> {
481
0
        if !self.graph_enabled {
482
0
            return None;
483
0
        }
484
0
        let node = ExecutionNode::Brick {
485
0
            id: brick_id,
486
0
            timing_ns,
487
0
            elements,
488
0
        };
489
0
        Some(self.execution_graph.add_node_in_scope(node))
490
0
    }
491
492
    /// Record a kernel launch in the execution graph.
493
    ///
494
    /// # Arguments
495
    /// - `name`: Kernel name (e.g., "batched_q4k_gemv")
496
    /// - `ptx_hash`: FNV-1a hash of PTX source for identity
497
    /// - `grid`: Grid dimensions (blocks)
498
    /// - `block`: Block dimensions (threads)
499
    /// - `shared_mem`: Shared memory bytes
500
0
    pub fn graph_record_kernel(
501
0
        &mut self,
502
0
        name: &str,
503
0
        ptx_hash: u64,
504
0
        grid: (u32, u32, u32),
505
0
        block: (u32, u32, u32),
506
0
        shared_mem: u32,
507
0
    ) -> Option<ExecutionNodeId> {
508
0
        if !self.graph_enabled {
509
0
            return None;
510
0
        }
511
0
        Some(
512
0
            self.execution_graph
513
0
                .record_kernel_launch(name, ptx_hash, grid, block, shared_mem),
514
0
        )
515
0
    }
516
517
    /// Export execution graph to DOT format for visualization.
518
    ///
519
    /// Use with Graphviz: `dot -Tsvg output.dot -o graph.svg`
520
    #[must_use]
521
0
    pub fn graph_to_dot(&self) -> String {
522
0
        self.execution_graph.to_dot()
523
0
    }
524
525
    /// Export execution graph to trueno-graph CsrGraph.
526
    #[cfg(feature = "execution-graph")]
527
    #[must_use]
528
    pub fn graph_to_csr(&self) -> trueno_graph::CsrGraph {
529
        self.execution_graph.to_csr()
530
    }
531
532
    /// Clear the execution graph.
533
0
    pub fn graph_clear(&mut self) {
534
0
        self.execution_graph.clear();
535
0
    }
536
537
    /// Check if the execution graph scope stack is balanced.
538
    #[must_use]
539
0
    pub fn graph_is_scope_balanced(&self) -> bool {
540
0
        self.execution_graph.is_scope_balanced()
541
0
    }
542
543
    /// Set L2 cache hit rate (v1.1.0 OBSERVE phase)
544
0
    pub fn set_l2_cache_hit_rate(&mut self, rate: f32) {
545
0
        self.l2_cache_hit_rate = Some(rate.clamp(0.0, 1.0));
546
0
    }
547
548
    /// Get L2 cache hit rate
549
0
    pub fn l2_cache_hit_rate(&self) -> Option<f32> {
550
0
        self.l2_cache_hit_rate
551
0
    }
552
553
    /// Set zero-copy mode (v1.1.0 OBSERVE phase)
554
0
    pub fn set_zero_copy(&mut self, enabled: bool) {
555
0
        self.is_zero_copy = enabled;
556
0
    }
557
558
    /// Check if zero-copy is enabled
559
0
    pub fn is_zero_copy(&self) -> bool {
560
0
        self.is_zero_copy
561
0
    }
562
563
    /// Enable profiling.
564
0
    pub fn enable(&mut self) {
565
0
        self.enabled = true;
566
0
    }
567
568
    /// Disable profiling.
569
0
    pub fn disable(&mut self) {
570
0
        self.enabled = false;
571
0
    }
572
573
    /// Check if profiling is enabled.
574
    #[must_use]
575
0
    pub fn is_enabled(&self) -> bool {
576
0
        self.enabled
577
0
    }
578
579
    /// Start timing a brick. Returns timer handle.
580
    ///
581
    /// IMPORTANT: For GPU operations, call sync AFTER the operation
582
    /// completes but BEFORE calling stop().
583
    #[must_use]
584
0
    pub fn start(&self, name: &str) -> BrickTimer {
585
0
        BrickTimer {
586
0
            name: name.to_string(),
587
0
            start: Instant::now(),
588
0
        }
589
0
    }
590
591
    /// Stop timing and record the sample.
592
    ///
593
    /// # Arguments
594
    /// - `timer`: Timer handle from `start()`
595
    /// - `elements`: Number of elements (tokens) processed
596
0
    pub fn stop(&mut self, timer: BrickTimer, elements: u64) {
597
0
        if !self.enabled {
598
0
            return;
599
0
        }
600
601
0
        let elapsed = timer.start.elapsed();
602
0
        let elapsed_ns = elapsed.as_nanos() as u64;
603
604
        // PAR-200: Try fast path first if name matches a known BrickId
605
0
        if let Some(brick_id) = BrickId::from_str(&timer.name) {
606
0
            let stats = &mut self.brick_stats[brick_id as usize];
607
0
            stats.add_sample(elapsed_ns, elements);
608
0
        } else {
609
            // Fall back to dynamic stats
610
0
            let name = timer.name;
611
0
            let stats = self
612
0
                .dynamic_stats
613
0
                .entry(name.clone())
614
0
                .or_insert_with(|| BrickStats::new(&name));
615
0
            stats.add_sample(elapsed_ns, elements);
616
        }
617
618
        // Update totals
619
0
        self.total_tokens += elements;
620
0
        self.total_ns += elapsed_ns;
621
0
    }
622
623
    /// Record a pre-measured duration for a brick.
624
    ///
625
    /// PAR-073: This method allows timing with raw `Instant` calls, avoiding
626
    /// borrow conflicts when profiling CUDA operations that also need `&mut self`.
627
    ///
628
    /// # Arguments
629
    /// - `name`: Brick name
630
    /// - `elapsed`: Duration of the operation (from `Instant::elapsed()`)
631
    /// - `elements`: Number of elements (tokens) processed
632
    ///
633
    /// # Example
634
    /// ```rust,ignore
635
    /// let start = std::time::Instant::now();
636
    /// cuda_stream.synchronize()?;
637
    /// self.some_cuda_operation()?;
638
    /// cuda_stream.synchronize()?;
639
    /// let elapsed = start.elapsed();
640
    /// self.profiler.record_elapsed("SomeBrick", elapsed, 1);
641
    /// ```
642
0
    pub fn record_elapsed(&mut self, name: &str, elapsed: std::time::Duration, elements: u64) {
643
0
        if !self.enabled {
644
0
            return;
645
0
        }
646
647
0
        let elapsed_ns = elapsed.as_nanos() as u64;
648
649
        // PAR-200: Try fast path first if name matches a known BrickId
650
0
        if let Some(brick_id) = BrickId::from_str(name) {
651
0
            let stats = &mut self.brick_stats[brick_id as usize];
652
0
            stats.add_sample(elapsed_ns, elements);
653
0
        } else {
654
            // Fall back to dynamic stats
655
0
            let stats = self
656
0
                .dynamic_stats
657
0
                .entry(name.to_string())
658
0
                .or_insert_with(|| BrickStats::new(name));
659
0
            stats.add_sample(elapsed_ns, elements);
660
        }
661
662
        // Update totals
663
0
        self.total_tokens += elements;
664
0
        self.total_ns += elapsed_ns;
665
0
    }
666
667
    /// PMAT-451: Record elapsed time with byte metrics for compression workloads.
668
    ///
669
    /// # Arguments
670
    /// - `name`: Brick name
671
    /// - `elapsed`: Duration of the operation
672
    /// - `elements`: Number of elements (pages) processed
673
    /// - `input_bytes`: Original uncompressed size
674
    /// - `output_bytes`: Compressed output size
675
    ///
676
    /// # Example
677
    /// ```rust,ignore
678
    /// let start = std::time::Instant::now();
679
    /// let compressed = zstd_compress(&page_data);
680
    /// let elapsed = start.elapsed();
681
    /// profiler.record_elapsed_with_bytes(
682
    ///     "ZstdCompress",
683
    ///     elapsed,
684
    ///     1,
685
    ///     page_data.len() as u64,
686
    ///     compressed.len() as u64,
687
    /// );
688
    /// ```
689
0
    pub fn record_elapsed_with_bytes(
690
0
        &mut self,
691
0
        name: &str,
692
0
        elapsed: std::time::Duration,
693
0
        elements: u64,
694
0
        input_bytes: u64,
695
0
        output_bytes: u64,
696
0
    ) {
697
0
        if !self.enabled {
698
0
            return;
699
0
        }
700
701
0
        let elapsed_ns = elapsed.as_nanos() as u64;
702
703
        // PAR-200: Try fast path first if name matches a known BrickId
704
0
        if let Some(brick_id) = BrickId::from_str(name) {
705
0
            let stats = &mut self.brick_stats[brick_id as usize];
706
0
            stats.add_sample_with_bytes(elapsed_ns, elements, input_bytes, output_bytes);
707
0
        } else {
708
            // Fall back to dynamic stats
709
0
            let stats = self
710
0
                .dynamic_stats
711
0
                .entry(name.to_string())
712
0
                .or_insert_with(|| BrickStats::new(name));
713
0
            stats.add_sample_with_bytes(elapsed_ns, elements, input_bytes, output_bytes);
714
        }
715
716
        // Update totals
717
0
        self.total_tokens += elements;
718
0
        self.total_ns += elapsed_ns;
719
0
    }
720
721
    /// PMAT-451: Set bottleneck classification for a brick.
722
0
    pub fn set_brick_bottleneck(&mut self, name: &str, bottleneck: BrickBottleneck) {
723
        // PAR-200: Try fast path first
724
0
        if let Some(brick_id) = BrickId::from_str(name) {
725
0
            self.brick_stats[brick_id as usize].set_bottleneck(bottleneck);
726
0
        } else if let Some(stats) = self.dynamic_stats.get_mut(name) {
727
0
            stats.set_bottleneck(bottleneck);
728
0
        }
729
0
    }
730
731
    /// Get statistics for a specific brick by name.
732
    ///
733
    /// First checks known BrickId types (O(1)), then falls back to dynamic stats.
734
    #[must_use]
735
0
    pub fn stats(&self, name: &str) -> Option<&BrickStats> {
736
        // Try fast path first
737
0
        if let Some(brick_id) = BrickId::from_str(name) {
738
0
            let stats = &self.brick_stats[brick_id as usize];
739
0
            if stats.count > 0 {
740
0
                return Some(stats);
741
0
            }
742
0
        }
743
        // Fall back to dynamic stats
744
0
        self.dynamic_stats.get(name)
745
0
    }
746
747
    /// Get all brick statistics (legacy API, returns dynamic stats only).
748
    ///
749
    /// For full statistics including known bricks, use `all_brick_stats()` instead.
750
    #[must_use]
751
    #[deprecated(
752
        since = "0.12.0",
753
        note = "Use all_brick_stats() for complete statistics"
754
    )]
755
0
    pub fn all_stats(&self) -> &std::collections::HashMap<String, BrickStats> {
756
0
        &self.dynamic_stats
757
0
    }
758
759
    /// Get all brick statistics including both known and dynamic bricks.
760
0
    pub fn all_brick_stats(&self) -> impl Iterator<Item = &BrickStats> {
761
0
        self.brick_stats
762
0
            .iter()
763
0
            .filter(|s| s.count > 0)
764
0
            .chain(self.dynamic_stats.values())
765
0
    }
766
767
    /// Get total throughput across all bricks.
768
    #[must_use]
769
0
    pub fn total_throughput(&self) -> f64 {
770
0
        if self.total_ns == 0 {
771
0
            0.0
772
        } else {
773
0
            self.total_tokens as f64 / (self.total_ns as f64 / 1_000_000_000.0)
774
        }
775
0
    }
776
777
    /// Get total tokens processed.
778
    #[must_use]
779
0
    pub fn total_tokens(&self) -> u64 {
780
0
        self.total_tokens
781
0
    }
782
783
    /// Get total time in nanoseconds.
784
    #[must_use]
785
0
    pub fn total_ns(&self) -> u64 {
786
0
        self.total_ns
787
0
    }
788
789
    /// Get all brick names.
790
    #[must_use]
791
0
    pub fn brick_names(&self) -> Vec<String> {
792
0
        let mut names: Vec<String> = self
793
0
            .brick_stats
794
0
            .iter()
795
0
            .enumerate()
796
0
            .filter(|(_, s)| s.count > 0)
797
0
            .map(|(i, _)| {
798
                // Safety: i < BrickId::COUNT
799
0
                let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
800
0
                brick_id.name().to_string()
801
0
            })
802
0
            .collect();
803
0
        names.extend(self.dynamic_stats.keys().cloned());
804
0
        names
805
0
    }
806
807
    /// Reset all statistics.
808
0
    pub fn reset(&mut self) {
809
0
        for stats in &mut self.brick_stats {
810
0
            stats.count = 0;
811
0
            stats.total_ns = 0;
812
0
            stats.min_ns = u64::MAX;
813
0
            stats.max_ns = 0;
814
0
            stats.total_elements = 0;
815
0
            stats.total_bytes = 0;
816
0
            stats.total_compressed_bytes = 0;
817
0
        }
818
0
        self.dynamic_stats.clear();
819
0
        self.pending.clear();
820
0
        self.total_tokens = 0;
821
0
        self.total_ns = 0;
822
0
    }
823
824
    /// Generate a summary report.
825
    #[must_use]
826
0
    pub fn summary(&self) -> String {
827
0
        let mut report = String::new();
828
0
        report.push_str("=== Brick Profiler Summary (PAR-200) ===\n");
829
0
        report.push_str(&format!(
830
0
            "Total: {} tokens, {:.2}µs, {:.1} tok/s\n",
831
0
            self.total_tokens,
832
0
            self.total_ns as f64 / 1000.0,
833
0
            self.total_throughput()
834
0
        ));
835
0
        report.push_str("\nPer-Brick Breakdown:\n");
836
837
        // Collect all stats (known + dynamic)
838
0
        let mut all_stats: Vec<(&str, &BrickStats)> = Vec::new();
839
840
        // Add known bricks with non-zero counts
841
0
        for (i, stats) in self.brick_stats.iter().enumerate() {
842
0
            if stats.count > 0 {
843
0
                // Safety: i < BrickId::COUNT
844
0
                let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
845
0
                all_stats.push((brick_id.name(), stats));
846
0
            }
847
        }
848
849
        // Add dynamic bricks
850
0
        for (name, stats) in &self.dynamic_stats {
851
0
            all_stats.push((name.as_str(), stats));
852
0
        }
853
854
        // Sort by total time descending
855
0
        all_stats.sort_by(|a, b| b.1.total_ns.cmp(&a.1.total_ns));
856
857
0
        for (name, stats) in all_stats {
858
0
            let pct = if self.total_ns > 0 {
859
0
                100.0 * stats.total_ns as f64 / self.total_ns as f64
860
            } else {
861
0
                0.0
862
            };
863
0
            report.push_str(&format!(
864
0
                "  {:20} {:8.2}µs avg ({:5.1}%) [{} samples]\n",
865
0
                name,
866
0
                stats.avg_us(),
867
0
                pct,
868
0
                stats.count
869
0
            ));
870
        }
871
872
        // Add category breakdown
873
0
        report.push_str("\nCategory Breakdown:\n");
874
0
        let cats = self.category_stats();
875
0
        for (i, cat_stats) in cats.iter().enumerate() {
876
0
            if cat_stats.count > 0 {
877
0
                // Safety: i < BrickCategory::COUNT
878
0
                let cat = unsafe { std::mem::transmute::<u8, BrickCategory>(i as u8) };
879
0
                report.push_str(&format!(
880
0
                    "  {:12} {:8.2}µs avg ({:5.1}%)\n",
881
0
                    cat.name(),
882
0
                    cat_stats.avg_us(),
883
0
                    cat_stats.percentage(self.total_ns)
884
0
                ));
885
0
            }
886
        }
887
888
0
        report
889
0
    }
890
891
    /// Export profiling data as JSON for pmat metrics integration.
892
    ///
893
    /// Format compatible with `.pmat-metrics/trends/` structure:
894
    /// ```json
895
    /// {
896
    ///   "total_tokens": 1000,
897
    ///   "total_ns": 5000000,
898
    ///   "total_throughput": 200000.0,
899
    ///   "bricks": [
900
    ///     {
901
    ///       "name": "RmsNorm",
902
    ///       "count": 10,
903
    ///       "total_ns": 1000000,
904
    ///       "avg_us": 100.0,
905
    ///       "min_us": 90.0,
906
    ///       "max_us": 120.0,
907
    ///       "throughput": 10000.0,
908
    ///       "pct": 20.0
909
    ///     }
910
    ///   ]
911
    /// }
912
    /// ```
913
    #[must_use]
914
0
    pub fn to_json(&self) -> String {
915
0
        let mut bricks = Vec::new();
916
917
        // Collect all stats (known + dynamic)
918
0
        let mut all_stats: Vec<(&str, &BrickStats)> = Vec::new();
919
920
        // Add known bricks with non-zero counts
921
0
        for (i, stats) in self.brick_stats.iter().enumerate() {
922
0
            if stats.count > 0 {
923
0
                // Safety: i < BrickId::COUNT
924
0
                let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
925
0
                all_stats.push((brick_id.name(), stats));
926
0
            }
927
        }
928
929
        // Add dynamic bricks
930
0
        for (name, stats) in &self.dynamic_stats {
931
0
            all_stats.push((name.as_str(), stats));
932
0
        }
933
934
        // Sort by total time descending
935
0
        all_stats.sort_by(|a, b| b.1.total_ns.cmp(&a.1.total_ns));
936
937
0
        for (name, stats) in all_stats {
938
0
            let pct = if self.total_ns > 0 {
939
0
                100.0 * stats.total_ns as f64 / self.total_ns as f64
940
            } else {
941
0
                0.0
942
            };
943
            // PMAT-451: Include compression_ratio, throughput_gbps, and bottleneck
944
0
            let compression = stats.compression_ratio();
945
0
            let throughput_gbps = stats.throughput_gbps();
946
0
            let bottleneck = stats.get_bottleneck();
947
0
            bricks.push(format!(
948
0
                r#"{{"name":"{}","count":{},"total_ns":{},"avg_us":{:.2},"min_us":{:.2},"max_us":{:.2},"throughput":{:.1},"pct":{:.1},"total_bytes":{},"compression_ratio":{:.2},"throughput_gbps":{:.2},"bottleneck":"{}"}}"#,
949
                name,
950
                stats.count,
951
                stats.total_ns,
952
0
                stats.avg_us(),
953
0
                stats.min_us(),
954
0
                stats.max_us(),
955
0
                stats.throughput(),
956
                pct,
957
                stats.total_bytes,
958
                compression,
959
                throughput_gbps,
960
                bottleneck
961
            ));
962
        }
963
964
0
        format!(
965
0
            r#"{{"total_tokens":{},"total_ns":{},"total_throughput":{:.1},"bricks":[{}]}}"#,
966
            self.total_tokens,
967
            self.total_ns,
968
0
            self.total_throughput(),
969
0
            bricks.join(",")
970
        )
971
0
    }
972
973
    /// Write profiling data to a JSON file for pmat tracking.
974
    ///
975
    /// # Errors
976
    /// Returns error if file cannot be written.
977
0
    pub fn write_json(&self, path: &std::path::Path) -> std::io::Result<()> {
978
0
        std::fs::write(path, self.to_json())
979
0
    }
980
981
    // =======================================================================
982
    // CORRECTNESS-011: Per-kernel checksum capture for divergence detection
983
    // =======================================================================
984
985
    /// Record a kernel trace with output checksum for divergence detection.
986
    ///
987
    /// This enables automated CPU/GPU divergence detection by capturing
988
    /// output checksums alongside timing data. When GPU produces wrong output,
989
    /// this identifies WHICH kernel diverged without hours of manual debugging.
990
    ///
991
    /// Five-Whys Root Cause: Hours of manual "let me check X in Y" debugging
992
    /// → No automated tool identified which kernel diverged
993
    /// → BrickProfiler only captured timing, not checksums
994
    /// → Missing feature: per-kernel checksum capture
995
    ///
996
    /// # Arguments
997
    /// - `name`: Brick/kernel name
998
    /// - `layer_idx`: Layer index (0-N for transformer layers)
999
    /// - `position`: Position in sequence
1000
    /// - `output`: Output tensor data (first 64 floats checksummed)
1001
    ///
1002
    /// # Example
1003
    /// ```rust,ignore
1004
    /// // After RoPE kernel
1005
    /// profiler.record_checksum("RopeNeox", layer_idx, position, &q_rotated);
1006
    /// ```
1007
0
    pub fn record_checksum(&mut self, name: &str, layer_idx: usize, position: u32, output: &[f32]) {
1008
0
        if !self.enabled {
1009
0
            return;
1010
0
        }
1011
0
        let checksum = fnv1a_f32_checksum(output);
1012
0
        let trace = KernelChecksum {
1013
0
            name: name.to_string(),
1014
0
            layer_idx,
1015
0
            position,
1016
0
            checksum,
1017
0
        };
1018
0
        self.kernel_checksums.push(trace);
1019
0
    }
1020
1021
    /// Get all kernel checksums for divergence comparison.
1022
    #[must_use]
1023
0
    pub fn get_checksums(&self) -> &[KernelChecksum] {
1024
0
        &self.kernel_checksums
1025
0
    }
1026
1027
    /// Compare checksums with a reference profiler (e.g., CPU baseline).
1028
    ///
1029
    /// Returns None if all checksums match, or the first divergent kernel.
1030
    #[must_use]
1031
0
    pub fn find_divergence(&self, reference: &BrickProfiler) -> Option<DivergenceInfo> {
1032
        use std::collections::HashMap;
1033
1034
        // Index reference checksums by (name, layer_idx, position)
1035
0
        let ref_index: HashMap<(&str, usize, u32), u64> = reference
1036
0
            .kernel_checksums
1037
0
            .iter()
1038
0
            .map(|t| ((t.name.as_str(), t.layer_idx, t.position), t.checksum))
1039
0
            .collect();
1040
1041
        // Check each of our checksums against reference
1042
0
        for trace in &self.kernel_checksums {
1043
0
            let key = (trace.name.as_str(), trace.layer_idx, trace.position);
1044
0
            if let Some(&expected) = ref_index.get(&key) {
1045
0
                if trace.checksum != expected {
1046
0
                    return Some(DivergenceInfo {
1047
0
                        kernel_name: trace.name.clone(),
1048
0
                        layer_idx: trace.layer_idx,
1049
0
                        position: trace.position,
1050
0
                        expected_checksum: expected,
1051
0
                        actual_checksum: trace.checksum,
1052
0
                    });
1053
0
                }
1054
0
            }
1055
        }
1056
0
        None
1057
0
    }
1058
1059
    /// Reset checksum tracking (call before new forward pass).
1060
0
    pub fn reset_checksums(&mut self) {
1061
0
        self.kernel_checksums.clear();
1062
0
    }
1063
1064
    // ========================================================================
1065
    // TILING-SPEC-001: Tile-Level Profiling (Phase 15)
1066
    // ========================================================================
1067
1068
    /// Enable tile-level profiling.
1069
    ///
1070
    /// When enabled, `start_tile()`/`stop_tile()` record per-tile statistics
1071
    /// for Macro/Midi/Micro tile hierarchy.
1072
0
    pub fn enable_tile_profiling(&mut self) {
1073
0
        self.tile_profiling_enabled = true;
1074
0
    }
1075
1076
    /// Disable tile-level profiling.
1077
0
    pub fn disable_tile_profiling(&mut self) {
1078
0
        self.tile_profiling_enabled = false;
1079
0
    }
1080
1081
    /// Check if tile profiling is enabled.
1082
    #[must_use]
1083
0
    pub fn is_tile_profiling_enabled(&self) -> bool {
1084
0
        self.tile_profiling_enabled
1085
0
    }
1086
1087
    /// Start timing a tile execution.
1088
    ///
1089
    /// Returns a `TileTimer` that should be passed to `stop_tile()` after
1090
    /// the tile computation completes.
1091
    ///
1092
    /// # Arguments
1093
    /// - `level`: Tile hierarchy level (Macro/Midi/Micro)
1094
    /// - `row`: Row index within parent tile
1095
    /// - `col`: Column index within parent tile
1096
    ///
1097
    /// # Example
1098
    /// ```rust,ignore
1099
    /// let timer = profiler.start_tile(TileLevel::Macro, 0, 0);
1100
    /// // ... execute tile computation ...
1101
    /// profiler.stop_tile(timer, 256 * 256, 2 * 256 * 256 * 256);
1102
    /// ```
1103
    #[must_use]
1104
0
    pub fn start_tile(&self, level: TileLevel, row: u32, col: u32) -> TileTimer {
1105
0
        TileTimer {
1106
0
            level,
1107
0
            _row: row,
1108
0
            _col: col,
1109
0
            start: Instant::now(),
1110
0
        }
1111
0
    }
1112
1113
    /// Stop timing and record tile statistics.
1114
    ///
1115
    /// # Arguments
1116
    /// - `timer`: Timer handle from `start_tile()`
1117
    /// - `elements`: Number of elements processed by this tile
1118
    /// - `flops`: Number of floating-point operations performed
1119
0
    pub fn stop_tile(&mut self, timer: TileTimer, elements: u64, flops: u64) {
1120
0
        if !self.tile_profiling_enabled {
1121
0
            return;
1122
0
        }
1123
1124
0
        let elapsed_ns = timer.start.elapsed().as_nanos() as u64;
1125
0
        let idx = timer.level as usize;
1126
0
        self.tile_stats[idx].add_sample(elapsed_ns, elements, flops);
1127
0
    }
1128
1129
    /// Get tile statistics for a given level.
1130
    #[must_use]
1131
0
    pub fn tile_stats(&self, level: TileLevel) -> &TileStats {
1132
0
        &self.tile_stats[level as usize]
1133
0
    }
1134
1135
    /// Get mutable tile statistics for a given level.
1136
0
    pub fn tile_stats_mut(&mut self, level: TileLevel) -> &mut TileStats {
1137
0
        &mut self.tile_stats[level as usize]
1138
0
    }
1139
1140
    /// Get all tile statistics as a slice.
1141
    #[must_use]
1142
0
    pub fn all_tile_stats(&self) -> &[TileStats; 3] {
1143
0
        &self.tile_stats
1144
0
    }
1145
1146
    /// Reset tile statistics for all levels.
1147
0
    pub fn reset_tile_stats(&mut self) {
1148
0
        self.tile_stats = [
1149
0
            TileStats::new(TileLevel::Macro),
1150
0
            TileStats::new(TileLevel::Midi),
1151
0
            TileStats::new(TileLevel::Micro),
1152
0
        ];
1153
0
    }
1154
1155
    /// Generate tile profiling summary report.
1156
    ///
1157
    /// # Example Output
1158
    /// ```text
1159
    /// === Tile Profiling Summary (TILING-SPEC-001) ===
1160
    /// Level       Samples   Avg µs    GFLOP/s   AI      Elements
1161
    /// Macro           128    1234.5     12.34  0.50    1048576
1162
    /// Midi           2048      78.2     45.67  2.00      65536
1163
    /// Micro         32768       4.9     89.12  4.00       4096
1164
    /// ```
1165
    #[must_use]
1166
0
    pub fn tile_summary(&self) -> String {
1167
0
        let mut report = String::new();
1168
0
        report.push_str("=== Tile Profiling Summary (TILING-SPEC-001) ===\n");
1169
0
        report.push_str("Level       Samples   Avg µs    GFLOP/s   AI      Elements\n");
1170
1171
0
        for stats in &self.tile_stats {
1172
0
            if stats.count > 0 {
1173
0
                report.push_str(&format!(
1174
0
                    "{:8}  {:9}  {:8.1}  {:8.2}  {:4.2}  {:10}\n",
1175
0
                    stats.level.name(),
1176
0
                    stats.count,
1177
0
                    stats.avg_us(),
1178
0
                    stats.gflops(),
1179
0
                    stats.arithmetic_intensity(),
1180
0
                    stats.total_elements / stats.count.max(1)
1181
0
                ));
1182
0
            }
1183
        }
1184
1185
0
        report
1186
0
    }
1187
1188
    /// Export tile statistics as JSON.
1189
    ///
1190
    /// Compatible with pmat metrics integration.
1191
    #[must_use]
1192
0
    pub fn tile_stats_to_json(&self) -> String {
1193
0
        let tiles: Vec<String> = self
1194
0
            .tile_stats
1195
0
            .iter()
1196
0
            .filter(|s| s.count > 0)
1197
0
            .map(|s| {
1198
0
                format!(
1199
0
                    r#"{{"level":"{}","count":{},"total_ns":{},"avg_us":{:.2},"min_us":{:.2},"max_us":{:.2},"gflops":{:.2},"arithmetic_intensity":{:.2},"total_elements":{},"total_flops":{}}}"#,
1200
0
                    s.level.name(),
1201
                    s.count,
1202
                    s.total_ns,
1203
0
                    s.avg_us(),
1204
0
                    s.min_ns as f64 / 1000.0,
1205
0
                    s.max_ns as f64 / 1000.0,
1206
0
                    s.gflops(),
1207
0
                    s.arithmetic_intensity(),
1208
                    s.total_elements,
1209
                    s.total_flops
1210
                )
1211
0
            })
1212
0
            .collect();
1213
1214
0
        format!(
1215
0
            r#"{{"tile_profiling_enabled":{},"tiles":[{}]}}"#,
1216
            self.tile_profiling_enabled,
1217
0
            tiles.join(",")
1218
        )
1219
0
    }
1220
}