## Coverage Report

#### Created: 2026-01-23 22:55

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/trueno/src/brick/profiler.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
    use std::fmt;
    12
    use std::time::Instant;
    13
    14
    use super::exec_graph::{
    15
        BrickBottleneck, BrickCategory, BrickId, BrickStats, CategoryStats, ExecutionGraph,
    16
        ExecutionNode, ExecutionNodeId, SyncMode,
    17
    };
    18
    19
    // ============================================================================
    20
    // TILING-SPEC-001: Tile-Level Profiling Support
    21
    // ============================================================================
    22
    23
    /// Tile-level profiling statistics.
    24
    ///
    25
    /// Tracks per-tile performance metrics for hierarchical cache-blocked operations.
    26
    /// Used in conjunction with `TcbGeometry` and `TilingConfig` from the tiling module.
    27
    ///
    28
    /// # Example
    29
    ///
    30
    /// ```ignore
    31
    /// let mut profiler = BrickProfiler::new();
    32
    /// profiler.enable();
    33
    ///
    34
    /// let tile_timer = profiler.start_tile(TileLevel::Macro, 0, 0);
    35
    /// // ... execute tile ...
    36
    /// profiler.stop_tile(tile_timer, 1024 * 1024);
    37
    /// ```
    38
    #[derive(Debug, Clone, Default)]
    39
    pub struct TileStats {
    40
        /// Tile level (Macro/Midi/Micro)
    41
        pub level: TileLevel,
    42
        /// Total samples collected
    43
        pub count: u64,
    44
        /// Total elapsed time (nanoseconds)
    45
        pub total_ns: u64,
    46
        /// Min elapsed time (nanoseconds)
    47
        pub min_ns: u64,
    48
        /// Max elapsed time (nanoseconds)
    49
        pub max_ns: u64,
    50
        /// Total elements processed
    51
        pub total_elements: u64,
    52
        /// Total cache misses (estimated)
    53
        pub cache_misses: u64,
    54
        /// Total arithmetic operations
    55
        pub total_flops: u64,
    56
    }
    57
    58
    /// Tile hierarchy level for profiling.
    59
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    60
    pub enum TileLevel {
    61
        /// Macro-tile: L3 cache / GPU global memory
    62
        #[default]
    63
        Macro,
    64
        /// Midi-tile: L2 cache / GPU shared memory
    65
        Midi,
    66
        /// Micro-tile: Registers / SIMD lanes
    67
        Micro,
    68
    }
    69
    70
    impl TileLevel {
    71
        /// Get the name of this tile level.
    72
        #[must_use]
    73
    0
        pub const fn name(&self) -> &'static str {
    74
    0
            match self {
    75
    0
                TileLevel::Macro => "macro",
    76
    0
                TileLevel::Midi => "midi",
    77
    0
                TileLevel::Micro => "micro",
    78
            }
    79
    0
        }
    80
    }
    81
    82
    impl TileStats {
    83
        /// Create new tile stats for a given level.
    84
    0
        pub fn new(level: TileLevel) -> Self {
    85
    0
            Self {
    86
    0
                level,
    87
    0
                count: 0,
    88
    0
                total_ns: 0,
    89
    0
                min_ns: u64::MAX,
    90
    0
                max_ns: 0,
    91
    0
                total_elements: 0,
    92
    0
                cache_misses: 0,
    93
    0
                total_flops: 0,
    94
    0
            }
    95
    0
        }
    96
    97
        /// Add a sample to statistics.
    98
    0
        pub fn add_sample(&mut self, elapsed_ns: u64, elements: u64, flops: u64) {
    99
    0
            self.count += 1;
    100
    0
            self.total_ns += elapsed_ns;
    101
    0
            self.min_ns = self.min_ns.min(elapsed_ns);
    102
    0
            self.max_ns = self.max_ns.max(elapsed_ns);
    103
    0
            self.total_elements += elements;
    104
    0
            self.total_flops += flops;
    105
    0
        }
    106
    107
        /// Average time in microseconds.
    108
        #[must_use]
    109
    0
        pub fn avg_us(&self) -> f64 {
    110
    0
            if self.count == 0 {
    111
    0
                0.0
    112
            } else {
    113
    0
                self.total_ns as f64 / self.count as f64 / 1000.0
    114
            }
    115
    0
        }
    116
    117
        /// Throughput in elements/second.
    118
        #[must_use]
    119
    0
        pub fn throughput(&self) -> f64 {
    120
    0
            if self.total_ns == 0 {
    121
    0
                0.0
    122
            } else {
    123
    0
                self.total_elements as f64 / (self.total_ns as f64 / 1_000_000_000.0)
    124
            }
    125
    0
        }
    126
    127
        /// Compute throughput in GFLOP/s.
    128
        #[must_use]
    129
    0
        pub fn gflops(&self) -> f64 {
    130
    0
            if self.total_ns == 0 {
    131
    0
                0.0
    132
            } else {
    133
    0
                self.total_flops as f64 / (self.total_ns as f64 / 1_000_000_000.0) / 1e9
    134
            }
    135
    0
        }
    136
    137
        /// Arithmetic intensity (FLOP/byte) estimate.
    138
        ///
    139
        /// Assumes 4 bytes per element (f32).
    140
        #[must_use]
    141
    0
        pub fn arithmetic_intensity(&self) -> f64 {
    142
    0
            if self.total_elements == 0 {
    143
    0
                0.0
    144
            } else {
    145
    0
                self.total_flops as f64 / (self.total_elements as f64 * 4.0)
    146
            }
    147
    0
        }
    148
    149
        /// Estimated cache efficiency (0.0-1.0).
    150
        ///
    151
        /// Based on ratio of actual throughput vs theoretical peak.
    152
        #[must_use]
    153
    0
        pub fn cache_efficiency(&self, peak_gflops: f64) -> f64 {
    154
    0
            if peak_gflops <= 0.0 {
    155
    0
                0.0
    156
            } else {
    157
    0
                (self.gflops() / peak_gflops).min(1.0)
    158
            }
    159
    0
        }
    160
    }
    161
    162
    /// Timer handle for tile-level profiling.
    163
    #[derive(Debug)]
    164
    pub struct TileTimer {
    165
        /// Tile level
    166
        level: TileLevel,
    167
        /// Row index within parent tile (reserved for spatial analysis)
    168
        _row: u32,
    169
        /// Column index within parent tile (reserved for spatial analysis)
    170
        _col: u32,
    171
        /// Start time
    172
        start: Instant,
    173
    }
    174
    175
    /// Pending measurement for deferred sync mode.
    176
    #[derive(Debug, Clone)]
    177
    struct PendingMeasurement {
    178
        /// Brick ID (if known)
    179
        brick_id: Option<BrickId>,
    180
        /// Brick name (for dynamic bricks)
    181
        name: Option<String>,
    182
        /// Start time in nanoseconds (from Instant::now())
    183
        start_ns: u64,
    184
        /// Number of elements processed
    185
        elements: u64,
    186
    }
    187
    188
    /// Per-brick profiler using pure Rust timing.
    189
    ///
    190
    /// # Design (PAR-073, PAR-200)
    191
    ///
    192
    /// - Uses `std::time::Instant` for timing (no CUDA event FFI)
    193
    /// - PAR-200: O(1) hot path with `BrickId` enum + array storage
    194
    /// - GPU operations require explicit sync before timing point
    195
    /// - Supports deferred sync mode for low-overhead production profiling
    196
    /// - Aggregates statistics per brick name
    197
    ///
    198
    /// # Usage
    199
    ///
    200
    /// ```rust,ignore
    201
    /// use trueno::brick::{BrickProfiler, BrickId, SyncMode};
    202
    ///
    203
    /// let mut profiler = BrickProfiler::new();
    204
    /// profiler.enable();
    205
    ///
    206
    /// // Fast path: use BrickId for known bricks (PAR-200)
    207
    /// let timer = profiler.start_brick(BrickId::RmsNorm);
    208
    /// // ... do work ...
    209
    /// // For GPU: cuda_stream.synchronize() HERE
    210
    /// profiler.stop_brick(timer, 1);
    211
    ///
    212
    /// // Legacy path: string-based (slower, for unknown bricks)
    213
    /// let timer = profiler.start("CustomBrick");
    214
    /// profiler.stop(timer, 1);
    215
    ///
    216
    /// // Deferred sync mode (production)
    217
    /// profiler.set_sync_mode(SyncMode::Deferred);
    218
    /// profiler.record_deferred(BrickId::RmsNorm, start_ns, 1);
    219
    /// // ... more operations ...
    220
    /// cuda_stream.synchronize();
    221
    /// profiler.finalize(end_ns);
    222
    ///
    223
    /// // Get statistics
    224
    /// let stats = profiler.brick_stats(BrickId::RmsNorm);
    225
    /// println!("RmsNorm avg: {:.2}µs", stats.avg_us());
    226
    ///
    227
    /// // Get category breakdown
    228
    /// let cats = profiler.category_stats();
    229
    /// println!("Attention: {:.1}%", cats[BrickCategory::Attention as usize].percentage(profiler.total_ns()));
    230
    /// ```
    231
    #[derive(Debug)]
    232
    pub struct BrickProfiler {
    233
        // PAR-200: Fast path - pre-allocated array for known bricks
    234
        /// Per-brick statistics for known BrickId types (O(1) lookup)
    235
        brick_stats: [BrickStats; BrickId::COUNT],
    236
    237
        // Legacy path - HashMap for dynamic/unknown brick names
    238
        /// Per-brick statistics for unknown brick names (slower, O(1) amortized)
    239
        dynamic_stats: std::collections::HashMap<String, BrickStats>,
    240
    241
        // PAR-200: Deferred sync support
    242
        /// Pending measurements awaiting GPU sync
    243
        pending: Vec<PendingMeasurement>,
    244
        /// Synchronization mode
    245
        sync_mode: SyncMode,
    246
        /// Reference instant for deferred timing
    247
        epoch: Instant,
    248
    249
        /// Whether profiling is enabled
    250
        enabled: bool,
    251
        /// Total tokens processed
    252
        total_tokens: u64,
    253
        /// Total time (ns) across all bricks
    254
        total_ns: u64,
    255
        /// L2 cache hit rate (0.0-1.0) - v1.1.0 OBSERVE phase
    256
        l2_cache_hit_rate: Option<f32>,
    257
        /// Whether zero-copy memory transfers are enabled - v1.1.0 OBSERVE phase
    258
        is_zero_copy: bool,
    259
        /// CORRECTNESS-011: Per-kernel checksums for divergence detection
    260
        kernel_checksums: Vec<KernelChecksum>,
    261
    262
        // PAR-201: Execution path graph
    263
        /// Whether execution graph tracking is enabled
    264
        graph_enabled: bool,
    265
        /// Execution path graph for PTX→kernel→brick relationships
    266
        execution_graph: ExecutionGraph,
    267
    268
        // TILING-SPEC-001: Tile-level profiling
    269
        /// Per-level tile statistics (Macro, Midi, Micro)
    270
        tile_stats: [TileStats; 3],
    271
        /// Whether tile profiling is enabled
    272
        tile_profiling_enabled: bool,
    273
    }
    274
    275
    /// Timer handle returned by `start()` (legacy string-based API).
    276
    #[derive(Debug)]
    277
    pub struct BrickTimer {
    278
        /// Brick name
    279
        name: String,
    280
        /// Start time
    281
        start: Instant,
    282
    }
    283
    284
    /// Timer handle returned by `start_brick()` (PAR-200 fast path).
    285
    #[derive(Debug)]
    286
    pub struct BrickIdTimer {
    287
        /// Brick ID
    288
        brick_id: BrickId,
    289
        /// Start time
    290
        start: Instant,
    291
    }
    292
    293
    impl Default for BrickProfiler {
    294
    0
        fn default() -> Self {
    295
    0
            Self::new()
    296
    0
        }
    297
    }
    298
    299
    impl BrickProfiler {
    300
        /// Create a new profiler (disabled by default for zero overhead).
    301
    0
        pub fn new() -> Self {
    302
            Self {
    303
    0
                brick_stats: std::array::from_fn(|i| {
    304
                    // Safety: i < BrickId::COUNT by construction
    305
    0
                    let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
    306
    0
                    BrickStats::new(brick_id.name())
    307
    0
                }),
    308
    0
                dynamic_stats: std::collections::HashMap::new(),
    309
    0
                pending: Vec::new(),
    310
    0
                sync_mode: SyncMode::Deferred,
    311
    0
                epoch: Instant::now(),
    312
                enabled: false,
    313
                total_tokens: 0,
    314
                total_ns: 0,
    315
    0
                l2_cache_hit_rate: None,
    316
                is_zero_copy: false,
    317
    0
                kernel_checksums: Vec::new(),
    318
                graph_enabled: false,
    319
    0
                execution_graph: ExecutionGraph::new(),
    320
    0
                tile_stats: [
    321
    0
                    TileStats::new(TileLevel::Macro),
    322
    0
                    TileStats::new(TileLevel::Midi),
    323
    0
                    TileStats::new(TileLevel::Micro),
    324
    0
                ],
    325
                tile_profiling_enabled: false,
    326
            }
    327
    0
        }
    328
    329
        /// Create an enabled profiler.
    330
    0
        pub fn enabled() -> Self {
    331
    0
            let mut profiler = Self::new();
    332
    0
            profiler.enabled = true;
    333
    0
            profiler
    334
    0
        }
    335
    336
        // ========================================================================
    337
        // PAR-200: Sync Mode Configuration
    338
        // ========================================================================
    339
    340
        /// Set the synchronization mode for GPU profiling.
    341
        ///
    342
        /// # Modes
    343
        /// - `Immediate`: Sync after each kernel (accurate but slow)
    344
        /// - `PerLayer`: Sync once per transformer layer
    345
        /// - `Deferred`: Sync once per forward pass (default, fast)
    346
        /// - `None`: No synchronization
    347
    0
        pub fn set_sync_mode(&mut self, mode: SyncMode) {
    348
    0
            self.sync_mode = mode;
    349
    0
        }
    350
    351
        /// Get the current synchronization mode.
    352
        #[must_use]
    353
    0
        pub fn sync_mode(&self) -> SyncMode {
    354
    0
            self.sync_mode
    355
    0
        }
    356
    357
        /// Reset the epoch for deferred timing.
    358
        /// Call this at the start of a forward pass.
    359
    0
        pub fn reset_epoch(&mut self) {
    360
    0
            self.epoch = Instant::now();
    361
    0
        }
    362
    363
        /// Get nanoseconds elapsed since epoch.
    364
        #[inline]
    365
    0
        pub fn elapsed_ns(&self) -> u64 {
    366
    0
            self.epoch.elapsed().as_nanos() as u64
    367
    0
        }
    368
    369
        // ========================================================================
    370
        // PAR-200: Fast Path API (BrickId-based)
    371
        // ========================================================================
    372
    373
        /// Start timing a brick using BrickId (O(1) hot path).
    374
        ///
    375
        /// This is the preferred API for known brick types.
    376
        /// For GPU operations, call `stream.synchronize()` before `stop_brick()`.
    377
        #[inline]
    378
        #[must_use]
    379
    0
        pub fn start_brick(&self, brick_id: BrickId) -> BrickIdTimer {
    380
    0
            BrickIdTimer {
    381
    0
                brick_id,
    382
    0
                start: Instant::now(),
    383
    0
            }
    384
    0
        }
    385
    386
        /// Stop timing and record the sample (O(1) hot path).
    387
        #[inline]
    388
    0
        pub fn stop_brick(&mut self, timer: BrickIdTimer, elements: u64) {
    389
    0
            if !self.enabled {
    390
    0
                return;
    391
    0
            }
    392
    393
    0
            let elapsed = timer.start.elapsed();
    394
    0
            let elapsed_ns = elapsed.as_nanos() as u64;
    395
    396
            // O(1) array access
    397
    0
            let stats = &mut self.brick_stats[timer.brick_id as usize];
    398
    0
            stats.add_sample(elapsed_ns, elements);
    399
    400
            // Update totals
    401
    0
            self.total_tokens += elements;
    402
    0
            self.total_ns += elapsed_ns;
    403
    0
        }
    404
    405
        /// Get statistics for a known brick type (O(1)).
    406
        #[inline]
    407
        #[must_use]
    408
    0
        pub fn brick_stats(&self, brick_id: BrickId) -> &BrickStats {
    409
    0
            &self.brick_stats[brick_id as usize]
    410
    0
        }
    411
    412
        /// Get mutable statistics for a known brick type (O(1)).
    413
        #[inline]
    414
    0
        pub fn brick_stats_mut(&mut self, brick_id: BrickId) -> &mut BrickStats {
    415
    0
            &mut self.brick_stats[brick_id as usize]
    416
    0
        }
    417
    418
        // ========================================================================
    419
        // PAR-200: Deferred Sync API
    420
        // ========================================================================
    421
    422
        /// Record a measurement without GPU sync (deferred mode).
    423
        ///
    424
        /// Call `finalize()` after GPU sync to apply all pending measurements.
    425
        ///
    426
        /// # Arguments
    427
        /// - `brick_id`: The brick type
    428
        /// - `start_ns`: Start time (from `elapsed_ns()` at operation start)
    429
        /// - `elements`: Number of elements processed
    430
        #[inline]
    431
    0
        pub fn record_deferred(&mut self, brick_id: BrickId, start_ns: u64, elements: u64) {
    432
    0
            if !self.enabled {
    433
    0
                return;
    434
    0
            }
    435
    0
            self.pending.push(PendingMeasurement {
    436
    0
                brick_id: Some(brick_id),
    437
    0
                name: None,
    438
    0
                start_ns,
    439
    0
                elements,
    440
    0
            });
    441
    0
        }
    442
    443
        /// Record a measurement for a dynamic brick (deferred mode).
    444
        #[inline]
    445
    0
        pub fn record_deferred_dynamic(&mut self, name: &str, start_ns: u64, elements: u64) {
    446
    0
            if !self.enabled {
    447
    0
                return;
    448
    0
            }
    449
    0
            self.pending.push(PendingMeasurement {
    450
    0
                brick_id: BrickId::from_str(name),
    451
    0
                name: Some(name.to_string()),
    452
    0
                start_ns,
    453
    0
                elements,
    454
    0
            });
    455
    0
        }
    456
    457
        /// Finalize all pending measurements after GPU sync.
    458
        ///
    459
        /// Must be called after `stream.synchronize()` to get accurate timing.
    460
        ///
    461
        /// # Arguments
    462
        /// - `end_ns`: End time (from `elapsed_ns()` after sync)
    463
    0
        pub fn finalize(&mut self, end_ns: u64) {
    464
    0
            if self.pending.is_empty() {
    465
    0
                return;
    466
    0
            }
    467
    468
            // Calculate elapsed time for each pending measurement
    469
    0
            for m in self.pending.drain(..) {
    470
    0
                let elapsed_ns = end_ns.saturating_sub(m.start_ns);
    471
    472
    0
                if let Some(brick_id) = m.brick_id {
    473
    0
                    // Fast path: known brick
    474
    0
                    let stats = &mut self.brick_stats[brick_id as usize];
    475
    0
                    stats.add_sample(elapsed_ns, m.elements);
    476
    0
                } else if let Some(name) = m.name {
    477
                    // Slow path: dynamic brick
    478
    0
                    let stats = self
    479
    0
                        .dynamic_stats
    480
    0
                        .entry(name.clone())
    481
    0
                        .or_insert_with(|| BrickStats::new(&name));
    482
    0
                    stats.add_sample(elapsed_ns, m.elements);
    483
    0
                }
    484
    485
    0
                self.total_tokens += m.elements;
    486
    0
                self.total_ns += elapsed_ns;
    487
            }
    488
    0
        }
    489
    490
        /// Check if there are pending measurements.
    491
        #[inline]
    492
        #[must_use]
    493
    0
        pub fn has_pending(&self) -> bool {
    494
    0
            !self.pending.is_empty()
    495
    0
        }
    496
    497
        /// Get number of pending measurements.
    498
        #[inline]
    499
        #[must_use]
    500
    0
        pub fn pending_count(&self) -> usize {
    501
    0
            self.pending.len()
    502
    0
        }
    503
    504
        // ========================================================================
    505
        // PAR-200: Category Aggregation
    506
        // ========================================================================
    507
    508
        /// Get aggregated statistics by category.
    509
        ///
    510
        /// Returns an array indexed by `BrickCategory as usize`.
    511
        #[must_use]
    512
    0
        pub fn category_stats(&self) -> [CategoryStats; BrickCategory::COUNT] {
    513
    0
            let mut result = [CategoryStats::default(); BrickCategory::COUNT];
    514
    515
    0
            for (i, stats) in self.brick_stats.iter().enumerate() {
    516
    0
                // Safety: i < BrickId::COUNT by construction
    517
    0
                let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
    518
    0
                let cat = brick_id.category() as usize;
    519
    0
                result[cat].total_ns += stats.total_ns;
    520
    0
                result[cat].total_elements += stats.total_elements;
    521
    0
                result[cat].count += stats.count;
    522
    0
            }
    523
    524
            // Include dynamic stats in "Other" category
    525
    0
            for stats in self.dynamic_stats.values() {
    526
    0
                let cat = BrickCategory::Other as usize;
    527
    0
                result[cat].total_ns += stats.total_ns;
    528
    0
                result[cat].total_elements += stats.total_elements;
    529
    0
                result[cat].count += stats.count;
    530
    0
            }
    531
    532
    0
            result
    533
    0
        }
    534
    535
        /// Print category breakdown to console.
    536
    0
        pub fn print_category_stats(&self) {
    537
    0
            let cats = self.category_stats();
    538
    0
            let total = self.total_ns;
    539
    540
    0
            println!("╭─────────────────────────────────────────────────────────╮");
    541
    0
            println!("│            Category Breakdown (PAR-200)                 │");
    542
    0
            println!("├─────────────────────────────────────────────────────────┤");
    543
    0
            for (i, cat_stats) in cats.iter().enumerate() {
    544
                // Safety: i < BrickCategory::COUNT
    545
    0
                let cat = unsafe { std::mem::transmute::<u8, BrickCategory>(i as u8) };
    546
    0
                if cat_stats.count > 0 {
    547
    0
                    println!(
    548
    0
                        "│ {:12} {:8.2}µs avg {:6.1}% [{:5} samples]        │",
    549
    0
                        cat.name(),
    550
    0
                        cat_stats.avg_us(),
    551
    0
                        cat_stats.percentage(total),
    552
    0
                        cat_stats.count
    553
    0
                    );
    554
    0
                }
    555
            }
    556
    0
            println!("╰─────────────────────────────────────────────────────────╯");
    557
    0
        }
    558
    559
        // ========================================================================
    560
        // PAR-201: Execution Path Graph
    561
        // ========================================================================
    562
    563
        /// Enable execution graph tracking.
    564
        ///
    565
        /// When enabled, the profiler records the execution hierarchy:
    566
        /// - Layer → Brick → Kernel relationships
    567
        /// - PTX hashes for kernel identity
    568
        /// - Timing data per node
    569
    0
        pub fn enable_graph(&mut self) {
    570
    0
            self.graph_enabled = true;
    571
    0
        }
    572
    573
        /// Disable execution graph tracking.
    574
    0
        pub fn disable_graph(&mut self) {
    575
    0
            self.graph_enabled = false;
    576
    0
        }
    577
    578
        /// Check if execution graph tracking is enabled.
    579
        #[must_use]
    580
    0
        pub fn is_graph_enabled(&self) -> bool {
    581
    0
            self.graph_enabled
    582
    0
        }
    583
    584
        /// Get the execution graph (immutable).
    585
        #[must_use]
    586
    0
        pub fn execution_graph(&self) -> &ExecutionGraph {
    587
    0
            &self.execution_graph
    588
    0
        }
    589
    590
        /// Get the execution graph (mutable).
    591
    0
        pub fn execution_graph_mut(&mut self) -> &mut ExecutionGraph {
    592
    0
            &mut self.execution_graph
    593
    0
        }
    594
    595
        /// Push a scope for hierarchical graph recording.
    596
        ///
    597
        /// # Example
    598
        ///
    599
        /// ```rust,ignore
    600
        /// profiler.enable_graph();
    601
        /// profiler.graph_push_scope(ExecutionNode::Layer { index: 0 });
    602
        /// // ... record bricks and kernels ...
    603
        /// profiler.graph_pop_scope();
    604
        /// ```
    605
    0
        pub fn graph_push_scope(&mut self, node: ExecutionNode) -> Option<ExecutionNodeId> {
    606
    0
            if !self.graph_enabled {
    607
    0
                return None;
    608
    0
            }
    609
    0
            Some(self.execution_graph.push_scope(node))
    610
    0
        }
    611
    612
        /// Pop the current scope.
    613
    0
        pub fn graph_pop_scope(&mut self) -> Option<ExecutionNodeId> {
    614
    0
            if !self.graph_enabled {
    615
    0
                return None;
    616
    0
            }
    617
    0
            self.execution_graph.pop_scope()
    618
    0
        }
    619
    620
        /// Record a brick in the execution graph.
    621
        ///
    622
        /// This should be called after `stop_brick()` with the timing data.
    623
    0
        pub fn graph_record_brick(
    624
    0
            &mut self,
    625
    0
            brick_id: BrickId,
    626
    0
            timing_ns: u64,
    627
    0
            elements: u64,
    628
    0
        ) -> Option<ExecutionNodeId> {
    629
    0
            if !self.graph_enabled {
    630
    0
                return None;
    631
    0
            }
    632
    0
            let node = ExecutionNode::Brick {
    633
    0
                id: brick_id,
    634
    0
                timing_ns,
    635
    0
                elements,
    636
    0
            };
    637
    0
            Some(self.execution_graph.add_node_in_scope(node))
    638
    0
        }
    639
    640
        /// Record a kernel launch in the execution graph.
    641
        ///
    642
        /// # Arguments
    643
        /// - `name`: Kernel name (e.g., "batched_q4k_gemv")
    644
        /// - `ptx_hash`: FNV-1a hash of PTX source for identity
    645
        /// - `grid`: Grid dimensions (blocks)
    646
        /// - `block`: Block dimensions (threads)
    647
        /// - `shared_mem`: Shared memory bytes
    648
    0
        pub fn graph_record_kernel(
    649
    0
            &mut self,
    650
    0
            name: &str,
    651
    0
            ptx_hash: u64,
    652
    0
            grid: (u32, u32, u32),
    653
    0
            block: (u32, u32, u32),
    654
    0
            shared_mem: u32,
    655
    0
        ) -> Option<ExecutionNodeId> {
    656
    0
            if !self.graph_enabled {
    657
    0
                return None;
    658
    0
            }
    659
    0
            Some(
    660
    0
                self.execution_graph
    661
    0
                    .record_kernel_launch(name, ptx_hash, grid, block, shared_mem),
    662
    0
            )
    663
    0
        }
    664
    665
        /// Export execution graph to DOT format for visualization.
    666
        ///
    667
        /// Use with Graphviz: `dot -Tsvg output.dot -o graph.svg`
    668
        #[must_use]
    669
    0
        pub fn graph_to_dot(&self) -> String {
    670
    0
            self.execution_graph.to_dot()
    671
    0
        }
    672
    673
        /// Export execution graph to trueno-graph CsrGraph.
    674
        #[cfg(feature = "execution-graph")]
    675
        #[must_use]
    676
        pub fn graph_to_csr(&self) -> trueno_graph::CsrGraph {
    677
            self.execution_graph.to_csr()
    678
        }
    679
    680
        /// Clear the execution graph.
    681
    0
        pub fn graph_clear(&mut self) {
    682
    0
            self.execution_graph.clear();
    683
    0
        }
    684
    685
        /// Check if the execution graph scope stack is balanced.
    686
        #[must_use]
    687
    0
        pub fn graph_is_scope_balanced(&self) -> bool {
    688
    0
            self.execution_graph.is_scope_balanced()
    689
    0
        }
    690
    691
        /// Set L2 cache hit rate (v1.1.0 OBSERVE phase)
    692
    0
        pub fn set_l2_cache_hit_rate(&mut self, rate: f32) {
    693
    0
            self.l2_cache_hit_rate = Some(rate.clamp(0.0, 1.0));
    694
    0
        }
    695
    696
        /// Get L2 cache hit rate
    697
    0
        pub fn l2_cache_hit_rate(&self) -> Option<f32> {
    698
    0
            self.l2_cache_hit_rate
    699
    0
        }
    700
    701
        /// Set zero-copy mode (v1.1.0 OBSERVE phase)
    702
    0
        pub fn set_zero_copy(&mut self, enabled: bool) {
    703
    0
            self.is_zero_copy = enabled;
    704
    0
        }
    705
    706
        /// Check if zero-copy is enabled
    707
    0
        pub fn is_zero_copy(&self) -> bool {
    708
    0
            self.is_zero_copy
    709
    0
        }
    710
    711
        /// Enable profiling.
    712
    0
        pub fn enable(&mut self) {
    713
    0
            self.enabled = true;
    714
    0
        }
    715
    716
        /// Disable profiling.
    717
    0
        pub fn disable(&mut self) {
    718
    0
            self.enabled = false;
    719
    0
        }
    720
    721
        /// Check if profiling is enabled.
    722
        #[must_use]
    723
    0
        pub fn is_enabled(&self) -> bool {
    724
    0
            self.enabled
    725
    0
        }
    726
    727
        /// Start timing a brick. Returns timer handle.
    728
        ///
    729
        /// IMPORTANT: For GPU operations, call sync AFTER the operation
    730
        /// completes but BEFORE calling stop().
    731
        #[must_use]
    732
    0
        pub fn start(&self, name: &str) -> BrickTimer {
    733
    0
            BrickTimer {
    734
    0
                name: name.to_string(),
    735
    0
                start: Instant::now(),
    736
    0
            }
    737
    0
        }
    738
    739
        /// Stop timing and record the sample.
    740
        ///
    741
        /// # Arguments
    742
        /// - `timer`: Timer handle from `start()`
    743
        /// - `elements`: Number of elements (tokens) processed
    744
    0
        pub fn stop(&mut self, timer: BrickTimer, elements: u64) {
    745
    0
            if !self.enabled {
    746
    0
                return;
    747
    0
            }
    748
    749
    0
            let elapsed = timer.start.elapsed();
    750
    0
            let elapsed_ns = elapsed.as_nanos() as u64;
    751
    752
            // PAR-200: Try fast path first if name matches a known BrickId
    753
    0
            if let Some(brick_id) = BrickId::from_str(&timer.name) {
    754
    0
                let stats = &mut self.brick_stats[brick_id as usize];
    755
    0
                stats.add_sample(elapsed_ns, elements);
    756
    0
            } else {
    757
                // Fall back to dynamic stats
    758
    0
                let name = timer.name;
    759
    0
                let stats = self
    760
    0
                    .dynamic_stats
    761
    0
                    .entry(name.clone())
    762
    0
                    .or_insert_with(|| BrickStats::new(&name));
    763
    0
                stats.add_sample(elapsed_ns, elements);
    764
            }
    765
    766
            // Update totals
    767
    0
            self.total_tokens += elements;
    768
    0
            self.total_ns += elapsed_ns;
    769
    0
        }
    770
    771
        /// Record a pre-measured duration for a brick.
    772
        ///
    773
        /// PAR-073: This method allows timing with raw `Instant` calls, avoiding
    774
        /// borrow conflicts when profiling CUDA operations that also need `&mut self`.
    775
        ///
    776
        /// # Arguments
    777
        /// - `name`: Brick name
    778
        /// - `elapsed`: Duration of the operation (from `Instant::elapsed()`)
    779
        /// - `elements`: Number of elements (tokens) processed
    780
        ///
    781
        /// # Example
    782
        /// ```rust,ignore
    783
        /// let start = std::time::Instant::now();
    784
        /// cuda_stream.synchronize()?;
    785
        /// self.some_cuda_operation()?;
    786
        /// cuda_stream.synchronize()?;
    787
        /// let elapsed = start.elapsed();
    788
        /// self.profiler.record_elapsed("SomeBrick", elapsed, 1);
    789
        /// ```
    790
    0
        pub fn record_elapsed(&mut self, name: &str, elapsed: std::time::Duration, elements: u64) {
    791
    0
            if !self.enabled {
    792
    0
                return;
    793
    0
            }
    794
    795
    0
            let elapsed_ns = elapsed.as_nanos() as u64;
    796
    797
            // PAR-200: Try fast path first if name matches a known BrickId
    798
    0
            if let Some(brick_id) = BrickId::from_str(name) {
    799
    0
                let stats = &mut self.brick_stats[brick_id as usize];
    800
    0
                stats.add_sample(elapsed_ns, elements);
    801
    0
            } else {
    802
                // Fall back to dynamic stats
    803
    0
                let stats = self
    804
    0
                    .dynamic_stats
    805
    0
                    .entry(name.to_string())
    806
    0
                    .or_insert_with(|| BrickStats::new(name));
    807
    0
                stats.add_sample(elapsed_ns, elements);
    808
            }
    809
    810
            // Update totals
    811
    0
            self.total_tokens += elements;
    812
    0
            self.total_ns += elapsed_ns;
    813
    0
        }
    814
    815
        /// PMAT-451: Record elapsed time with byte metrics for compression workloads.
    816
        ///
    817
        /// # Arguments
    818
        /// - `name`: Brick name
    819
        /// - `elapsed`: Duration of the operation
    820
        /// - `elements`: Number of elements (pages) processed
    821
        /// - `input_bytes`: Original uncompressed size
    822
        /// - `output_bytes`: Compressed output size
    823
        ///
    824
        /// # Example
    825
        /// ```rust,ignore
    826
        /// let start = std::time::Instant::now();
    827
        /// let compressed = zstd_compress(&page_data);
    828
        /// let elapsed = start.elapsed();
    829
        /// profiler.record_elapsed_with_bytes(
    830
        ///     "ZstdCompress",
    831
        ///     elapsed,
    832
        ///     1,
    833
        ///     page_data.len() as u64,
    834
        ///     compressed.len() as u64,
    835
        /// );
    836
        /// ```
    837
    0
        pub fn record_elapsed_with_bytes(
    838
    0
            &mut self,
    839
    0
            name: &str,
    840
    0
            elapsed: std::time::Duration,
    841
    0
            elements: u64,
    842
    0
            input_bytes: u64,
    843
    0
            output_bytes: u64,
    844
    0
        ) {
    845
    0
            if !self.enabled {
    846
    0
                return;
    847
    0
            }
    848
    849
    0
            let elapsed_ns = elapsed.as_nanos() as u64;
    850
    851
            // PAR-200: Try fast path first if name matches a known BrickId
    852
    0
            if let Some(brick_id) = BrickId::from_str(name) {
    853
    0
                let stats = &mut self.brick_stats[brick_id as usize];
    854
    0
                stats.add_sample_with_bytes(elapsed_ns, elements, input_bytes, output_bytes);
    855
    0
            } else {
    856
                // Fall back to dynamic stats
    857
    0
                let stats = self
    858
    0
                    .dynamic_stats
    859
    0
                    .entry(name.to_string())
    860
    0
                    .or_insert_with(|| BrickStats::new(name));
    861
    0
                stats.add_sample_with_bytes(elapsed_ns, elements, input_bytes, output_bytes);
    862
            }
    863
    864
            // Update totals
    865
    0
            self.total_tokens += elements;
    866
    0
            self.total_ns += elapsed_ns;
    867
    0
        }
    868
    869
        /// PMAT-451: Set bottleneck classification for a brick.
    870
    0
        pub fn set_brick_bottleneck(&mut self, name: &str, bottleneck: BrickBottleneck) {
    871
            // PAR-200: Try fast path first
    872
    0
            if let Some(brick_id) = BrickId::from_str(name) {
    873
    0
                self.brick_stats[brick_id as usize].set_bottleneck(bottleneck);
    874
    0
            } else if let Some(stats) = self.dynamic_stats.get_mut(name) {
    875
    0
                stats.set_bottleneck(bottleneck);
    876
    0
            }
    877
    0
        }
    878
    879
        /// Get statistics for a specific brick by name.
    880
        ///
    881
        /// First checks known BrickId types (O(1)), then falls back to dynamic stats.
    882
        #[must_use]
    883
    0
        pub fn stats(&self, name: &str) -> Option<&BrickStats> {
    884
            // Try fast path first
    885
    0
            if let Some(brick_id) = BrickId::from_str(name) {
    886
    0
                let stats = &self.brick_stats[brick_id as usize];
    887
    0
                if stats.count > 0 {
    888
    0
                    return Some(stats);
    889
    0
                }
    890
    0
            }
    891
            // Fall back to dynamic stats
    892
    0
            self.dynamic_stats.get(name)
    893
    0
        }
    894
    895
        /// Get all brick statistics (legacy API, returns dynamic stats only).
    896
        ///
    897
        /// For full statistics including known bricks, use `all_brick_stats()` instead.
    898
        #[must_use]
    899
        #[deprecated(
    900
            since = "0.12.0",
    901
            note = "Use all_brick_stats() for complete statistics"
    902
        )]
    903
    0
        pub fn all_stats(&self) -> &std::collections::HashMap<String, BrickStats> {
    904
    0
            &self.dynamic_stats
    905
    0
        }
    906
    907
        /// Get all brick statistics including both known and dynamic bricks.
    908
    0
        pub fn all_brick_stats(&self) -> impl Iterator<Item = &BrickStats> {
    909
    0
            self.brick_stats
    910
    0
                .iter()
    911
    0
                .filter(|s| s.count > 0)
    912
    0
                .chain(self.dynamic_stats.values())
    913
    0
        }
    914
    915
        /// Get total throughput across all bricks.
    916
        #[must_use]
    917
    0
        pub fn total_throughput(&self) -> f64 {
    918
    0
            if self.total_ns == 0 {
    919
    0
                0.0
    920
            } else {
    921
    0
                self.total_tokens as f64 / (self.total_ns as f64 / 1_000_000_000.0)
    922
            }
    923
    0
        }
    924
    925
        /// Get total tokens processed.
    926
        #[must_use]
    927
    0
        pub fn total_tokens(&self) -> u64 {
    928
    0
            self.total_tokens
    929
    0
        }
    930
    931
        /// Get total time in nanoseconds.
    932
        #[must_use]
    933
    0
        pub fn total_ns(&self) -> u64 {
    934
    0
            self.total_ns
    935
    0
        }
    936
    937
        /// Get all brick names.
    938
        #[must_use]
    939
    0
        pub fn brick_names(&self) -> Vec<String> {
    940
    0
            let mut names: Vec<String> = self
    941
    0
                .brick_stats
    942
    0
                .iter()
    943
    0
                .enumerate()
    944
    0
                .filter(|(_, s)| s.count > 0)
    945
    0
                .map(|(i, _)| {
    946
                    // Safety: i < BrickId::COUNT
    947
    0
                    let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
    948
    0
                    brick_id.name().to_string()
    949
    0
                })
    950
    0
                .collect();
    951
    0
            names.extend(self.dynamic_stats.keys().cloned());
    952
    0
            names
    953
    0
        }
    954
    955
        /// Reset all statistics.
    956
    0
        pub fn reset(&mut self) {
    957
    0
            for stats in &mut self.brick_stats {
    958
    0
                stats.count = 0;
    959
    0
                stats.total_ns = 0;
    960
    0
                stats.min_ns = u64::MAX;
    961
    0
                stats.max_ns = 0;
    962
    0
                stats.total_elements = 0;
    963
    0
                stats.total_bytes = 0;
    964
    0
                stats.total_compressed_bytes = 0;
    965
    0
            }
    966
    0
            self.dynamic_stats.clear();
    967
    0
            self.pending.clear();
    968
    0
            self.total_tokens = 0;
    969
    0
            self.total_ns = 0;
    970
    0
        }
    971
    972
        /// Generate a summary report.
    973
        #[must_use]
    974
    0
        pub fn summary(&self) -> String {
    975
    0
            let mut report = String::new();
    976
    0
            report.push_str("=== Brick Profiler Summary (PAR-200) ===\n");
    977
    0
            report.push_str(&format!(
    978
    0
                "Total: {} tokens, {:.2}µs, {:.1} tok/s\n",
    979
    0
                self.total_tokens,
    980
    0
                self.total_ns as f64 / 1000.0,
    981
    0
                self.total_throughput()
    982
    0
            ));
    983
    0
            report.push_str("\nPer-Brick Breakdown:\n");
    984
    985
            // Collect all stats (known + dynamic)
    986
    0
            let mut all_stats: Vec<(&str, &BrickStats)> = Vec::new();
    987
    988
            // Add known bricks with non-zero counts
    989
    0
            for (i, stats) in self.brick_stats.iter().enumerate() {
    990
    0
                if stats.count > 0 {
    991
    0
                    // Safety: i < BrickId::COUNT
    992
    0
                    let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
    993
    0
                    all_stats.push((brick_id.name(), stats));
    994
    0
                }
    995
            }
    996
    997
            // Add dynamic bricks
    998
    0
            for (name, stats) in &self.dynamic_stats {
    999
    0
                all_stats.push((name.as_str(), stats));
    1000
    0
            }
    1001
    1002
            // Sort by total time descending
    1003
    0
            all_stats.sort_by(|a, b| b.1.total_ns.cmp(&a.1.total_ns));
    1004
    1005
    0
            for (name, stats) in all_stats {
    1006
    0
                let pct = if self.total_ns > 0 {
    1007
    0
                    100.0 * stats.total_ns as f64 / self.total_ns as f64
    1008
                } else {
    1009
    0
                    0.0
    1010
                };
    1011
    0
                report.push_str(&format!(
    1012
    0
                    "  {:20} {:8.2}µs avg ({:5.1}%) [{} samples]\n",
    1013
    0
                    name,
    1014
    0
                    stats.avg_us(),
    1015
    0
                    pct,
    1016
    0
                    stats.count
    1017
    0
                ));
    1018
            }
    1019
    1020
            // Add category breakdown
    1021
    0
            report.push_str("\nCategory Breakdown:\n");
    1022
    0
            let cats = self.category_stats();
    1023
    0
            for (i, cat_stats) in cats.iter().enumerate() {
    1024
    0
                if cat_stats.count > 0 {
    1025
    0
                    // Safety: i < BrickCategory::COUNT
    1026
    0
                    let cat = unsafe { std::mem::transmute::<u8, BrickCategory>(i as u8) };
    1027
    0
                    report.push_str(&format!(
    1028
    0
                        "  {:12} {:8.2}µs avg ({:5.1}%)\n",
    1029
    0
                        cat.name(),
    1030
    0
                        cat_stats.avg_us(),
    1031
    0
                        cat_stats.percentage(self.total_ns)
    1032
    0
                    ));
    1033
    0
                }
    1034
            }
    1035
    1036
    0
            report
    1037
    0
        }
    1038
    1039
        /// Export profiling data as JSON for pmat metrics integration.
    1040
        ///
    1041
        /// Format compatible with `.pmat-metrics/trends/` structure:
    1042
        /// ```json
    1043
        /// {
    1044
        ///   "total_tokens": 1000,
    1045
        ///   "total_ns": 5000000,
    1046
        ///   "total_throughput": 200000.0,
    1047
        ///   "bricks": [
    1048
        ///     {
    1049
        ///       "name": "RmsNorm",
    1050
        ///       "count": 10,
    1051
        ///       "total_ns": 1000000,
    1052
        ///       "avg_us": 100.0,
    1053
        ///       "min_us": 90.0,
    1054
        ///       "max_us": 120.0,
    1055
        ///       "throughput": 10000.0,
    1056
        ///       "pct": 20.0
    1057
        ///     }
    1058
        ///   ]
    1059
        /// }
    1060
        /// ```
    1061
        #[must_use]
    1062
    0
        pub fn to_json(&self) -> String {
    1063
    0
            let mut bricks = Vec::new();
    1064
    1065
            // Collect all stats (known + dynamic)
    1066
    0
            let mut all_stats: Vec<(&str, &BrickStats)> = Vec::new();
    1067
    1068
            // Add known bricks with non-zero counts
    1069
    0
            for (i, stats) in self.brick_stats.iter().enumerate() {
    1070
    0
                if stats.count > 0 {
    1071
    0
                    // Safety: i < BrickId::COUNT
    1072
    0
                    let brick_id = unsafe { std::mem::transmute::<u8, BrickId>(i as u8) };
    1073
    0
                    all_stats.push((brick_id.name(), stats));
    1074
    0
                }
    1075
            }
    1076
    1077
            // Add dynamic bricks
    1078
    0
            for (name, stats) in &self.dynamic_stats {
    1079
    0
                all_stats.push((name.as_str(), stats));
    1080
    0
            }
    1081
    1082
            // Sort by total time descending
    1083
    0
            all_stats.sort_by(|a, b| b.1.total_ns.cmp(&a.1.total_ns));
    1084
    1085
    0
            for (name, stats) in all_stats {
    1086
    0
                let pct = if self.total_ns > 0 {
    1087
    0
                    100.0 * stats.total_ns as f64 / self.total_ns as f64
    1088
                } else {
    1089
    0
                    0.0
    1090
                };
    1091
                // PMAT-451: Include compression_ratio, throughput_gbps, and bottleneck
    1092
    0
                let compression = stats.compression_ratio();
    1093
    0
                let throughput_gbps = stats.throughput_gbps();
    1094
    0
                let bottleneck = stats.get_bottleneck();
    1095
    0
                bricks.push(format!(
    1096
    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":"{}"}}"#,
    1097
                    name,
    1098
                    stats.count,
    1099
                    stats.total_ns,
    1100
    0
                    stats.avg_us(),
    1101
    0
                    stats.min_us(),
    1102
    0
                    stats.max_us(),
    1103
    0
                    stats.throughput(),
    1104
                    pct,
    1105
                    stats.total_bytes,
    1106
                    compression,
    1107
                    throughput_gbps,
    1108
                    bottleneck
    1109
                ));
    1110
            }
    1111
    1112
    0
            format!(
    1113
    0
                r#"{{"total_tokens":{},"total_ns":{},"total_throughput":{:.1},"bricks":[{}]}}"#,
    1114
                self.total_tokens,
    1115
                self.total_ns,
    1116
    0
                self.total_throughput(),
    1117
    0
                bricks.join(",")
    1118
            )
    1119
    0
        }
    1120
    1121
        /// Write profiling data to a JSON file for pmat tracking.
    1122
        ///
    1123
        /// # Errors
    1124
        /// Returns error if file cannot be written.
    1125
    0
        pub fn write_json(&self, path: &std::path::Path) -> std::io::Result<()> {
    1126
    0
            std::fs::write(path, self.to_json())
    1127
    0
        }
    1128
    1129
        // =======================================================================
    1130
        // CORRECTNESS-011: Per-kernel checksum capture for divergence detection
    1131
        // =======================================================================
    1132
    1133
        /// Record a kernel trace with output checksum for divergence detection.
    1134
        ///
    1135
        /// This enables automated CPU/GPU divergence detection by capturing
    1136
        /// output checksums alongside timing data. When GPU produces wrong output,
    1137
        /// this identifies WHICH kernel diverged without hours of manual debugging.
    1138
        ///
    1139
        /// Five-Whys Root Cause: Hours of manual "let me check X in Y" debugging
    1140
        /// → No automated tool identified which kernel diverged
    1141
        /// → BrickProfiler only captured timing, not checksums
    1142
        /// → Missing feature: per-kernel checksum capture
    1143
        ///
    1144
        /// # Arguments
    1145
        /// - `name`: Brick/kernel name
    1146
        /// - `layer_idx`: Layer index (0-N for transformer layers)
    1147
        /// - `position`: Position in sequence
    1148
        /// - `output`: Output tensor data (first 64 floats checksummed)
    1149
        ///
    1150
        /// # Example
    1151
        /// ```rust,ignore
    1152
        /// // After RoPE kernel
    1153
        /// profiler.record_checksum("RopeNeox", layer_idx, position, &q_rotated);
    1154
        /// ```
    1155
    0
        pub fn record_checksum(&mut self, name: &str, layer_idx: usize, position: u32, output: &[f32]) {
    1156
    0
            if !self.enabled {
    1157
    0
                return;
    1158
    0
            }
    1159
    0
            let checksum = fnv1a_f32_checksum(output);
    1160
    0
            let trace = KernelChecksum {
    1161
    0
                name: name.to_string(),
    1162
    0
                layer_idx,
    1163
    0
                position,
    1164
    0
                checksum,
    1165
    0
            };
    1166
    0
            self.kernel_checksums.push(trace);
    1167
    0
        }
    1168
    1169
        /// Get all kernel checksums for divergence comparison.
    1170
        #[must_use]
    1171
    0
        pub fn get_checksums(&self) -> &[KernelChecksum] {
    1172
    0
            &self.kernel_checksums
    1173
    0
        }
    1174
    1175
        /// Compare checksums with a reference profiler (e.g., CPU baseline).
    1176
        ///
    1177
        /// Returns None if all checksums match, or the first divergent kernel.
    1178
        #[must_use]
    1179
    0
        pub fn find_divergence(&self, reference: &BrickProfiler) -> Option<DivergenceInfo> {
    1180
            use std::collections::HashMap;
    1181
    1182
            // Index reference checksums by (name, layer_idx, position)
    1183
    0
            let ref_index: HashMap<(&str, usize, u32), u64> = reference
    1184
    0
                .kernel_checksums
    1185
    0
                .iter()
    1186
    0
                .map(|t| ((t.name.as_str(), t.layer_idx, t.position), t.checksum))
    1187
    0
                .collect();
    1188
    1189
            // Check each of our checksums against reference
    1190
    0
            for trace in &self.kernel_checksums {
    1191
    0
                let key = (trace.name.as_str(), trace.layer_idx, trace.position);
    1192
    0
                if let Some(&expected) = ref_index.get(&key) {
    1193
    0
                    if trace.checksum != expected {
    1194
    0
                        return Some(DivergenceInfo {
    1195
    0
                            kernel_name: trace.name.clone(),
    1196
    0
                            layer_idx: trace.layer_idx,
    1197
    0
                            position: trace.position,
    1198
    0
                            expected_checksum: expected,
    1199
    0
                            actual_checksum: trace.checksum,
    1200
    0
                        });
    1201
    0
                    }
    1202
    0
                }
    1203
            }
    1204
    0
            None
    1205
    0
        }
    1206
    1207
        /// Reset checksum tracking (call before new forward pass).
    1208
    0
        pub fn reset_checksums(&mut self) {
    1209
    0
            self.kernel_checksums.clear();
    1210
    0
        }
    1211
    1212
        // ========================================================================
    1213
        // TILING-SPEC-001: Tile-Level Profiling (Phase 15)
    1214
        // ========================================================================
    1215
    1216
        /// Enable tile-level profiling.
    1217
        ///
    1218
        /// When enabled, `start_tile()`/`stop_tile()` record per-tile statistics
    1219
        /// for Macro/Midi/Micro tile hierarchy.
    1220
    0
        pub fn enable_tile_profiling(&mut self) {
    1221
    0
            self.tile_profiling_enabled = true;
    1222
    0
        }
    1223
    1224
        /// Disable tile-level profiling.
    1225
    0
        pub fn disable_tile_profiling(&mut self) {
    1226
    0
            self.tile_profiling_enabled = false;
    1227
    0
        }
    1228
    1229
        /// Check if tile profiling is enabled.
    1230
        #[must_use]
    1231
    0
        pub fn is_tile_profiling_enabled(&self) -> bool {
    1232
    0
            self.tile_profiling_enabled
    1233
    0
        }
    1234
    1235
        /// Start timing a tile execution.
    1236
        ///
    1237
        /// Returns a `TileTimer` that should be passed to `stop_tile()` after
    1238
        /// the tile computation completes.
    1239
        ///
    1240
        /// # Arguments
    1241
        /// - `level`: Tile hierarchy level (Macro/Midi/Micro)
    1242
        /// - `row`: Row index within parent tile
    1243
        /// - `col`: Column index within parent tile
    1244
        ///
    1245
        /// # Example
    1246
        /// ```rust,ignore
    1247
        /// let timer = profiler.start_tile(TileLevel::Macro, 0, 0);
    1248
        /// // ... execute tile computation ...
    1249
        /// profiler.stop_tile(timer, 256 * 256, 2 * 256 * 256 * 256);
    1250
        /// ```
    1251
        #[must_use]
    1252
    0
        pub fn start_tile(&self, level: TileLevel, row: u32, col: u32) -> TileTimer {
    1253
    0
            TileTimer {
    1254
    0
                level,
    1255
    0
                _row: row,
    1256
    0
                _col: col,
    1257
    0
                start: Instant::now(),
    1258
    0
            }
    1259
    0
        }
    1260
    1261
        /// Stop timing and record tile statistics.
    1262
        ///
    1263
        /// # Arguments
    1264
        /// - `timer`: Timer handle from `start_tile()`
    1265
        /// - `elements`: Number of elements processed by this tile
    1266
        /// - `flops`: Number of floating-point operations performed
    1267
    0
        pub fn stop_tile(&mut self, timer: TileTimer, elements: u64, flops: u64) {
    1268
    0
            if !self.tile_profiling_enabled {
    1269
    0
                return;
    1270
    0
            }
    1271
    1272
    0
            let elapsed_ns = timer.start.elapsed().as_nanos() as u64;
    1273
    0
            let idx = timer.level as usize;
    1274
    0
            self.tile_stats[idx].add_sample(elapsed_ns, elements, flops);
    1275
    0
        }
    1276
    1277
        /// Get tile statistics for a given level.
    1278
        #[must_use]
    1279
    0
        pub fn tile_stats(&self, level: TileLevel) -> &TileStats {
    1280
    0
            &self.tile_stats[level as usize]
    1281
    0
        }
    1282
    1283
        /// Get mutable tile statistics for a given level.
    1284
    0
        pub fn tile_stats_mut(&mut self, level: TileLevel) -> &mut TileStats {
    1285
    0
            &mut self.tile_stats[level as usize]
    1286
    0
        }
    1287
    1288
        /// Get all tile statistics as a slice.
    1289
        #[must_use]
    1290
    0
        pub fn all_tile_stats(&self) -> &[TileStats; 3] {
    1291
    0
            &self.tile_stats
    1292
    0
        }
    1293
    1294
        /// Reset tile statistics for all levels.
    1295
    0
        pub fn reset_tile_stats(&mut self) {
    1296
    0
            self.tile_stats = [
    1297
    0
                TileStats::new(TileLevel::Macro),
    1298
    0
                TileStats::new(TileLevel::Midi),
    1299
    0
                TileStats::new(TileLevel::Micro),
    1300
    0
            ];
    1301
    0
        }
    1302
    1303
        /// Generate tile profiling summary report.
    1304
        ///
    1305
        /// # Example Output
    1306
        /// ```text
    1307
        /// === Tile Profiling Summary (TILING-SPEC-001) ===
    1308
        /// Level       Samples   Avg µs    GFLOP/s   AI      Elements
    1309
        /// Macro           128    1234.5     12.34  0.50    1048576
    1310
        /// Midi           2048      78.2     45.67  2.00      65536
    1311
        /// Micro         32768       4.9     89.12  4.00       4096
    1312
        /// ```
    1313
        #[must_use]
    1314
    0
        pub fn tile_summary(&self) -> String {
    1315
    0
            let mut report = String::new();
    1316
    0
            report.push_str("=== Tile Profiling Summary (TILING-SPEC-001) ===\n");
    1317
    0
            report.push_str("Level       Samples   Avg µs    GFLOP/s   AI      Elements\n");
    1318
    1319
    0
            for stats in &self.tile_stats {
    1320
    0
                if stats.count > 0 {
    1321
    0
                    report.push_str(&format!(
    1322
    0
                        "{:8}  {:9}  {:8.1}  {:8.2}  {:4.2}  {:10}\n",
    1323
    0
                        stats.level.name(),
    1324
    0
                        stats.count,
    1325
    0
                        stats.avg_us(),
    1326
    0
                        stats.gflops(),
    1327
    0
                        stats.arithmetic_intensity(),
    1328
    0
                        stats.total_elements / stats.count.max(1)
    1329
    0
                    ));
    1330
    0
                }
    1331
            }
    1332
    1333
    0
            report
    1334
    0
        }
    1335
    1336
        /// Export tile statistics as JSON.
    1337
        ///
    1338
        /// Compatible with pmat metrics integration.
    1339
        #[must_use]
    1340
    0
        pub fn tile_stats_to_json(&self) -> String {
    1341
    0
            let tiles: Vec<String> = self
    1342
    0
                .tile_stats
    1343
    0
                .iter()
    1344
    0
                .filter(|s| s.count > 0)
    1345
    0
                .map(|s| {
    1346
    0
                    format!(
    1347
    0
                        r#"{{"level":"{}","count":{},"total_ns":{},"avg_us":{:.2},"min_us":{:.2},"max_us":{:.2},"gflops":{:.2},"arithmetic_intensity":{:.2},"total_elements":{},"total_flops":{}}}"#,
    1348
    0
                        s.level.name(),
    1349
                        s.count,
    1350
                        s.total_ns,
    1351
    0
                        s.avg_us(),
    1352
    0
                        s.min_ns as f64 / 1000.0,
    1353
    0
                        s.max_ns as f64 / 1000.0,
    1354
    0
                        s.gflops(),
    1355
    0
                        s.arithmetic_intensity(),
    1356
                        s.total_elements,
    1357
                        s.total_flops
    1358
                    )
    1359
    0
                })
    1360
    0
                .collect();
    1361
    1362
    0
            format!(
    1363
    0
                r#"{{"tile_profiling_enabled":{},"tiles":[{}]}}"#,
    1364
                self.tile_profiling_enabled,
    1365
    0
                tiles.join(",")
    1366
            )
    1367
    0
        }
    1368
    }
    1369
    1370
    /// Kernel checksum for divergence detection.
    1371
    ///
    1372
    /// CORRECTNESS-011: Captures output checksum per kernel invocation.
    1373
    #[derive(Debug, Clone)]
    1374
    pub struct KernelChecksum {
    1375
        /// Kernel/brick name
    1376
        pub name: String,
    1377
        /// Layer index
    1378
        pub layer_idx: usize,
    1379
        /// Sequence position
    1380
        pub position: u32,
    1381
        /// FNV-1a checksum of first 64 output floats
    1382
        pub checksum: u64,
    1383
    }
    1384
    1385
    /// Information about a detected divergence between CPU and GPU.
    1386
    #[derive(Debug, Clone)]
    1387
    pub struct DivergenceInfo {
    1388
        /// Name of the divergent kernel
    1389
        pub kernel_name: String,
    1390
        /// Layer where divergence occurred
    1391
        pub layer_idx: usize,
    1392
        /// Position where divergence occurred
    1393
        pub position: u32,
    1394
        /// Expected checksum (from CPU/reference)
    1395
        pub expected_checksum: u64,
    1396
        /// Actual checksum (from GPU/test)
    1397
        pub actual_checksum: u64,
    1398
    }
    1399
    1400
    impl fmt::Display for DivergenceInfo {
    1401
    0
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    1402
    0
            write!(
    1403
    0
                f,
    1404
    0
                "DIVERGENCE at '{}' (layer {}, pos {}): expected 0x{:016X}, got 0x{:016X}",
    1405
                self.kernel_name,
    1406
                self.layer_idx,
    1407
                self.position,
    1408
                self.expected_checksum,
    1409
                self.actual_checksum
    1410
            )
    1411
    0
        }
    1412
    }
    1413
    1414
    /// FNV-1a hash of f32 slice (first 64 elements for efficiency).
    1415
    ///
    1416
    /// Used for quick divergence detection between CPU and GPU outputs.
    1417
    #[inline]
    1418
    0
    pub fn fnv1a_f32_checksum(data: &[f32]) -> u64 {
    1419
        const FNV_OFFSET: u64 = 0xcbf29ce484222325;
    1420
        const FNV_PRIME: u64 = 0x100000001b3;
    1421
    1422
    0
        let mut hash = FNV_OFFSET;
    1423
    0
        let len = data.len().min(64);
    1424
    0
        for &val in &data[..len] {
    1425
    0
            let bytes = val.to_le_bytes();
    1426
    0
            for byte in bytes {
    1427
    0
                hash ^= u64::from(byte);
    1428
    0
                hash = hash.wrapping_mul(FNV_PRIME);
    1429
    0
            }
    1430
        }
    1431
    0
        hash
    1432
    0
    }
    1433
    1434
    /// Macro for convenient brick timing with automatic sync.
    1435
    ///
    1436
    /// # Usage
    1437
    ///
    1438
    /// ```rust,ignore
    1439
    /// time_brick!(profiler, "RmsNorm", 1, {
    1440
    ///     rmsnorm_kernel.launch();
    1441
    ///     stream.synchronize(); // REQUIRED for GPU
    1442
    /// });
    1443
    /// ```
    1444
    #[macro_export]
    1445
    macro_rules! time_brick {
    1446
        ($profiler:expr, $name:expr, $elements:expr, $body:block) => {{
    1447
            let timer = $profiler.start($name);
    1448
            let result = $body;
    1449
            $profiler.stop(timer, $elements);
    1450
            result
    1451
        }};
    1452
    }
    1453
    1454
    #[cfg(test)]
    1455
    mod tests {
    1456
        use super::*;
    1457
    1458
        // ========================================================================
    1459
        // TileStats Tests
    1460
        // ========================================================================
    1461
    1462
        #[test]
    1463
        fn test_tile_stats_new() {
    1464
            let stats = TileStats::new(TileLevel::Macro);
    1465
            assert_eq!(stats.level, TileLevel::Macro);
    1466
            assert_eq!(stats.count, 0);
    1467
            assert_eq!(stats.total_ns, 0);
    1468
            assert_eq!(stats.min_ns, u64::MAX);
    1469
            assert_eq!(stats.max_ns, 0);
    1470
        }
    1471
    1472
        #[test]
    1473
        fn test_tile_stats_add_sample() {
    1474
            let mut stats = TileStats::new(TileLevel::Midi);
    1475
            stats.add_sample(1000, 100, 200);
    1476
            assert_eq!(stats.count, 1);
    1477
            assert_eq!(stats.total_ns, 1000);
    1478
            assert_eq!(stats.min_ns, 1000);
    1479
            assert_eq!(stats.max_ns, 1000);
    1480
            assert_eq!(stats.total_elements, 100);
    1481
            assert_eq!(stats.total_flops, 200);
    1482
    1483
            stats.add_sample(2000, 150, 300);
    1484
            assert_eq!(stats.count, 2);
    1485
            assert_eq!(stats.total_ns, 3000);
    1486
            assert_eq!(stats.min_ns, 1000);
    1487
            assert_eq!(stats.max_ns, 2000);
    1488
        }
    1489
    1490
        #[test]
    1491
        fn test_tile_stats_avg_us() {
    1492
            let mut stats = TileStats::new(TileLevel::Micro);
    1493
            assert_eq!(stats.avg_us(), 0.0);
    1494
    1495
            stats.add_sample(2000_000, 100, 200); // 2000 µs
    1496
            assert!((stats.avg_us() - 2000.0).abs() < 0.01);
    1497
    1498
            stats.add_sample(4000_000, 100, 200); // 4000 µs
    1499
            assert!((stats.avg_us() - 3000.0).abs() < 0.01); // (2000 + 4000) / 2
    1500
        }
    1501
    1502
        #[test]
    1503
        fn test_tile_stats_throughput() {
    1504
            let mut stats = TileStats::new(TileLevel::Macro);
    1505
            assert_eq!(stats.throughput(), 0.0);
    1506
    1507
            // 1 billion ns = 1 second, 1000 elements = 1000 elem/s
    1508
            stats.add_sample(1_000_000_000, 1000, 0);
    1509
            assert!((stats.throughput() - 1000.0).abs() < 1.0);
    1510
        }
    1511
    1512
        #[test]
    1513
        fn test_tile_stats_gflops() {
    1514
            let mut stats = TileStats::new(TileLevel::Macro);
    1515
            assert_eq!(stats.gflops(), 0.0);
    1516
    1517
            // 1 second, 1 billion FLOPs = 1 GFLOP/s
    1518
            stats.add_sample(1_000_000_000, 100, 1_000_000_000);
    1519
            assert!((stats.gflops() - 1.0).abs() < 0.01);
    1520
        }
    1521
    1522
        #[test]
    1523
        fn test_tile_stats_arithmetic_intensity() {
    1524
            let mut stats = TileStats::new(TileLevel::Midi);
    1525
            assert_eq!(stats.arithmetic_intensity(), 0.0);
    1526
    1527
            // 1000 elements * 4 bytes = 4000 bytes, 4000 flops = 1.0 AI
    1528
            stats.add_sample(1000, 1000, 4000);
    1529
            assert!((stats.arithmetic_intensity() - 1.0).abs() < 0.01);
    1530
        }
    1531
    1532
        #[test]
    1533
        fn test_tile_level_name() {
    1534
            assert_eq!(TileLevel::Macro.name(), "macro");
    1535
            assert_eq!(TileLevel::Midi.name(), "midi");
    1536
            assert_eq!(TileLevel::Micro.name(), "micro");
    1537
        }
    1538
    1539
        // ========================================================================
    1540
        // BrickProfiler Tests
    1541
        // ========================================================================
    1542
    1543
        #[test]
    1544
        fn test_brick_profiler_new() {
    1545
            let profiler = BrickProfiler::new();
    1546
            assert!(!profiler.is_enabled());
    1547
            assert_eq!(profiler.total_tokens(), 0);
    1548
            assert_eq!(profiler.total_ns(), 0);
    1549
        }
    1550
    1551
        #[test]
    1552
        fn test_brick_profiler_enabled() {
    1553
            let profiler = BrickProfiler::enabled();
    1554
            assert!(profiler.is_enabled());
    1555
        }
    1556
    1557
        #[test]
    1558
        fn test_brick_profiler_enable_disable() {
    1559
            let mut profiler = BrickProfiler::new();
    1560
            assert!(!profiler.is_enabled());
    1561
    1562
            profiler.enable();
    1563
            assert!(profiler.is_enabled());
    1564
    1565
            profiler.disable();
    1566
            assert!(!profiler.is_enabled());
    1567
        }
    1568
    1569
        #[test]
    1570
        fn test_brick_profiler_sync_mode() {
    1571
            let mut profiler = BrickProfiler::new();
    1572
            assert_eq!(profiler.sync_mode(), SyncMode::Deferred);
    1573
    1574
            profiler.set_sync_mode(SyncMode::Immediate);
    1575
            assert_eq!(profiler.sync_mode(), SyncMode::Immediate);
    1576
        }
    1577
    1578
        #[test]
    1579
        fn test_brick_profiler_start_stop() {
    1580
            let mut profiler = BrickProfiler::new();
    1581
            profiler.enable();
    1582
    1583
            let timer = profiler.start("TestBrick");
    1584
            std::thread::sleep(std::time::Duration::from_micros(100));
    1585
            profiler.stop(timer, 10);
    1586
    1587
            // Should have recorded something
    1588
            assert!(profiler.total_tokens() >= 10);
    1589
            assert!(profiler.total_ns() > 0);
    1590
        }
    1591
    1592
        #[test]
    1593
        fn test_brick_profiler_start_stop_disabled() {
    1594
            let mut profiler = BrickProfiler::new();
    1595
            // profiler is disabled by default
    1596
    1597
            let timer = profiler.start("TestBrick");
    1598
            profiler.stop(timer, 10);
    1599
    1600
            // Should NOT have recorded anything
    1601
            assert_eq!(profiler.total_tokens(), 0);
    1602
            assert_eq!(profiler.total_ns(), 0);
    1603
        }
    1604
    1605
        #[test]
    1606
        fn test_brick_profiler_brick_id_api() {
    1607
            let mut profiler = BrickProfiler::new();
    1608
            profiler.enable();
    1609
    1610
            let timer = profiler.start_brick(BrickId::RmsNorm);
    1611
            std::thread::sleep(std::time::Duration::from_micros(50));
    1612
            profiler.stop_brick(timer, 5);
    1613
    1614
            let stats = profiler.brick_stats(BrickId::RmsNorm);
    1615
            assert_eq!(stats.count, 1);
    1616
            assert!(stats.total_ns > 0);
    1617
        }
    1618
    1619
        #[test]
    1620
        fn test_brick_profiler_deferred_api() {
    1621
            let mut profiler = BrickProfiler::new();
    1622
            profiler.enable();
    1623
    1624
            let start_ns = profiler.elapsed_ns();
    1625
            std::thread::sleep(std::time::Duration::from_micros(100));
    1626
            profiler.record_deferred(BrickId::QkvProjection, start_ns, 100);
    1627
    1628
            assert!(profiler.has_pending());
    1629
            assert_eq!(profiler.pending_count(), 1);
    1630
    1631
            let end_ns = profiler.elapsed_ns();
    1632
            profiler.finalize(end_ns);
    1633
    1634
            assert!(!profiler.has_pending());
    1635
            assert_eq!(profiler.pending_count(), 0);
    1636
    1637
            let stats = profiler.brick_stats(BrickId::QkvProjection);
    1638
            assert_eq!(stats.count, 1);
    1639
        }
    1640
    1641
        #[test]
    1642
        fn test_brick_profiler_reset() {
    1643
            let mut profiler = BrickProfiler::new();
    1644
            profiler.enable();
    1645
    1646
            let timer = profiler.start_brick(BrickId::AttentionSoftmax);
    1647
            profiler.stop_brick(timer, 10);
    1648
    1649
            assert!(profiler.total_tokens() > 0);
    1650
    1651
            profiler.reset();
    1652
    1653
            assert_eq!(profiler.total_tokens(), 0);
    1654
            assert_eq!(profiler.total_ns(), 0);
    1655
        }
    1656
    1657
        #[test]
    1658
        fn test_brick_profiler_tile_profiling() {
    1659
            let mut profiler = BrickProfiler::new();
    1660
            assert!(!profiler.is_tile_profiling_enabled());
    1661
    1662
            profiler.enable_tile_profiling();
    1663
            assert!(profiler.is_tile_profiling_enabled());
    1664
    1665
            let timer = profiler.start_tile(TileLevel::Macro, 0, 0);
    1666
            std::thread::sleep(std::time::Duration::from_micros(50));
    1667
            profiler.stop_tile(timer, 1024, 2048);
    1668
    1669
            let stats = profiler.tile_stats(TileLevel::Macro);
    1670
            assert_eq!(stats.count, 1);
    1671
            assert!(stats.total_ns > 0);
    1672
        }
    1673
    1674
        #[test]
    1675
        fn test_brick_profiler_tile_profiling_disabled() {
    1676
            let mut profiler = BrickProfiler::new();
    1677
            // Tile profiling disabled by default
    1678
    1679
            let timer = profiler.start_tile(TileLevel::Midi, 1, 1);
    1680
            profiler.stop_tile(timer, 512, 1024);
    1681
    1682
            let stats = profiler.tile_stats(TileLevel::Midi);
    1683
            assert_eq!(stats.count, 0);
    1684
        }
    1685
    1686
        // ========================================================================
    1687
        // Checksum Tests (CORRECTNESS-011)
    1688
        // ========================================================================
    1689
    1690
        #[test]
    1691
        fn test_fnv1a_checksum_empty() {
    1692
            let data: [f32; 0] = [];
    1693
            let checksum = fnv1a_f32_checksum(&data);
    1694
            // Should return offset basis for empty input
    1695
            assert_eq!(checksum, 0xcbf29ce484222325);
    1696
        }
    1697
    1698
        #[test]
    1699
        fn test_fnv1a_checksum_deterministic() {
    1700
            let data = [1.0f32, 2.0, 3.0, 4.0];
    1701
            let c1 = fnv1a_f32_checksum(&data);
    1702
            let c2 = fnv1a_f32_checksum(&data);
    1703
            assert_eq!(c1, c2);
    1704
        }
    1705
    1706
        #[test]
    1707
        fn test_fnv1a_checksum_different_inputs() {
    1708
            let data1 = [1.0f32, 2.0, 3.0];
    1709
            let data2 = [1.0f32, 2.0, 4.0];
    1710
            let c1 = fnv1a_f32_checksum(&data1);
    1711
            let c2 = fnv1a_f32_checksum(&data2);
    1712
            assert_ne!(c1, c2);
    1713
        }
    1714
    1715
        #[test]
    1716
        fn test_fnv1a_checksum_truncates_at_64() {
    1717
            let data_short: Vec<f32> = (0..64).map(|i| i as f32).collect();
    1718
            let data_long: Vec<f32> = (0..100).map(|i| i as f32).collect();
    1719
    1720
            let c1 = fnv1a_f32_checksum(&data_short);
    1721
            let c2 = fnv1a_f32_checksum(&data_long);
    1722
            // Both should hash only first 64 elements
    1723
            assert_eq!(c1, c2);
    1724
        }
    1725
    1726
        #[test]
    1727
        fn test_brick_profiler_divergence_detection() {
    1728
            let mut cpu_profiler = BrickProfiler::new();
    1729
            let mut gpu_profiler = BrickProfiler::new();
    1730
            cpu_profiler.enable();
    1731
            gpu_profiler.enable();
    1732
    1733
            // Record same checksum on both
    1734
            let data = [1.0f32, 2.0, 3.0, 4.0];
    1735
            cpu_profiler.record_checksum("TestKernel", 0, 0, &data);
    1736
            gpu_profiler.record_checksum("TestKernel", 0, 0, &data);
    1737
    1738
            // No divergence
    1739
            assert!(gpu_profiler.find_divergence(&cpu_profiler).is_none());
    1740
    1741
            // Now record different checksum
    1742
            let different_data = [1.0f32, 2.0, 3.0, 5.0]; // Changed last element
    1743
            gpu_profiler.reset_checksums();
    1744
            gpu_profiler.record_checksum("TestKernel", 0, 0, &different_data);
    1745
    1746
            // Should find divergence
    1747
            let div = gpu_profiler.find_divergence(&cpu_profiler);
    1748
            assert!(div.is_some());
    1749
            let div = div.unwrap();
    1750
            assert_eq!(div.kernel_name, "TestKernel");
    1751
            assert_eq!(div.layer_idx, 0);
    1752
            assert_eq!(div.position, 0);
    1753
        }
    1754
    1755
        // ========================================================================
    1756
        // Falsification Tests
    1757
        // ========================================================================
    1758
    1759
        /// FALSIFICATION TEST: TileStats min/max monotonicity
    1760
        ///
    1761
        /// After any sequence of add_sample calls, min_ns <= max_ns must hold.
    1762
        #[test]
    1763
        fn test_falsify_tile_stats_min_max_monotonicity() {
    1764
            let mut stats = TileStats::new(TileLevel::Macro);
    1765
    1766
            // Add samples with varying elapsed times
    1767
            for ns in [1000, 500, 2000, 100, 5000, 50] {
    1768
                stats.add_sample(ns, 10, 20);
    1769
                assert!(
    1770
                    stats.min_ns <= stats.max_ns,
    1771
                    "FALSIFICATION FAILED: min {} > max {} after adding {}",
    1772
                    stats.min_ns,
    1773
                    stats.max_ns,
    1774
                    ns
    1775
                );
    1776
            }
    1777
    1778
            assert_eq!(stats.min_ns, 50);
    1779
            assert_eq!(stats.max_ns, 5000);
    1780
        }
    1781
    1782
        /// FALSIFICATION TEST: BrickProfiler total_tokens accumulation
    1783
        ///
    1784
        /// total_tokens must equal sum of all elements passed to stop/stop_brick.
    1785
        #[test]
    1786
        fn test_falsify_total_tokens_accumulation() {
    1787
            let mut profiler = BrickProfiler::new();
    1788
            profiler.enable();
    1789
    1790
            let mut expected_total = 0u64;
    1791
            let element_counts = [10, 20, 30, 15, 25];
    1792
    1793
            for &count in &element_counts {
    1794
                let timer = profiler.start_brick(BrickId::RmsNorm);
    1795
                profiler.stop_brick(timer, count);
    1796
                expected_total += count;
    1797
            }
    1798
    1799
            assert_eq!(
    1800
                profiler.total_tokens(),
    1801
                expected_total,
    1802
                "FALSIFICATION FAILED: total_tokens {} != expected {}",
    1803
                profiler.total_tokens(),
    1804
                expected_total
    1805
            );
    1806
        }
    1807
    1808
        /// FALSIFICATION TEST: Checksum collision resistance
    1809
        ///
    1810
        /// Different float patterns should produce different checksums.
    1811
        #[test]
    1812
        fn test_falsify_checksum_collision_resistance() {
    1813
            // Generate various patterns that might collide in weak hashes
    1814
            let patterns: Vec<Vec<f32>> = vec![
    1815
                vec![0.0; 10],
    1816
                vec![1.0; 10],
    1817
                vec![-1.0; 10],
    1818
                (0..10).map(|i| i as f32).collect(),
    1819
                (0..10).map(|i| -(i as f32)).collect(),
    1820
                vec![f32::MIN_POSITIVE; 10],
    1821
                vec![f32::MAX; 10],
    1822
            ];
    1823
    1824
            let checksums: Vec<u64> = patterns.iter().map(|p| fnv1a_f32_checksum(p)).collect();
    1825
    1826
            // Check all pairs for uniqueness
    1827
            for i in 0..checksums.len() {
    1828
                for j in (i + 1)..checksums.len() {
    1829
                    assert_ne!(
    1830
                        checksums[i], checksums[j],
    1831
                        "FALSIFICATION FAILED: patterns {} and {} collide with checksum {:016X}",
    1832
                        i, j, checksums[i]
    1833
                    );
    1834
                }
    1835
            }
    1836
        }
    1837
    1838
        // ========================================================================
    1839
        // Additional Coverage Tests
    1840
        // ========================================================================
    1841
    1842
        #[test]
    1843
        fn test_tile_stats_cache_efficiency() {
    1844
            let mut stats = TileStats::new(TileLevel::Macro);
    1845
    1846
            // Zero peak_gflops should return 0.0
    1847
            assert_eq!(stats.cache_efficiency(0.0), 0.0);
    1848
            assert_eq!(stats.cache_efficiency(-1.0), 0.0);
    1849
    1850
            // 1 second, 1 billion FLOPs = 1 GFLOP/s
    1851
            stats.add_sample(1_000_000_000, 100, 1_000_000_000);
    1852
            let efficiency = stats.cache_efficiency(10.0); // 10 GFLOP/s peak
    1853
            assert!((efficiency - 0.1).abs() < 0.01);
    1854
    1855
            // Efficiency capped at 1.0
    1856
            let capped = stats.cache_efficiency(0.5); // Lower peak than actual
    1857
            assert!((capped - 1.0).abs() < 0.001);
    1858
        }
    1859
    1860
        #[test]
    1861
        fn test_brick_profiler_record_elapsed() {
    1862
            let mut profiler = BrickProfiler::new();
    1863
            profiler.enable();
    1864
    1865
            let duration = std::time::Duration::from_micros(1000);
    1866
            profiler.record_elapsed("MyBrick", duration, 100);
    1867
    1868
            assert!(profiler.total_tokens() >= 100);
    1869
            assert!(profiler.total_ns() > 0);
    1870
        }
    1871
    1872
        #[test]
    1873
        fn test_brick_profiler_record_elapsed_disabled() {
    1874
            let mut profiler = BrickProfiler::new();
    1875
            // profiler is disabled
    1876
    1877
            let duration = std::time::Duration::from_micros(1000);
    1878
            profiler.record_elapsed("MyBrick", duration, 100);
    1879
    1880
            // Should not record when disabled
    1881
            assert_eq!(profiler.total_tokens(), 0);
    1882
        }
    1883
    1884
        #[test]
    1885
        fn test_brick_profiler_record_elapsed_with_bytes() {
    1886
            let mut profiler = BrickProfiler::new();
    1887
            profiler.enable();
    1888
    1889
            let duration = std::time::Duration::from_micros(500);
    1890
            profiler.record_elapsed_with_bytes("ByteBrick", duration, 200, 4096, 2048);
    1891
    1892
            assert!(profiler.total_tokens() >= 200);
    1893
        }
    1894
    1895
        #[test]
    1896
        fn test_brick_profiler_set_brick_bottleneck() {
    1897
            let mut profiler = BrickProfiler::new();
    1898
            profiler.enable();
    1899
    1900
            // Record something first
    1901
            let timer = profiler.start("TestBrick");
    1902
            profiler.stop(timer, 10);
    1903
    1904
            profiler.set_brick_bottleneck("TestBrick", BrickBottleneck::Memory);
    1905
            // Should not panic
    1906
        }
    1907
    1908
        #[test]
    1909
        fn test_brick_profiler_category_stats() {
    1910
            let mut profiler = BrickProfiler::new();
    1911
            profiler.enable();
    1912
    1913
            // Record normalization brick
    1914
            let timer = profiler.start_brick(BrickId::RmsNorm);
    1915
            std::thread::sleep(std::time::Duration::from_micros(10));
    1916
            profiler.stop_brick(timer, 100);
    1917
    1918
            let category_stats = profiler.category_stats();
    1919
            // Should have accumulated in Norm category
    1920
            let norm_stats = &category_stats[BrickCategory::Norm as usize];
    1921
            assert!(norm_stats.count >= 1);
    1922
        }
    1923
    1924
        #[test]
    1925
        fn test_brick_profiler_graph_operations() {
    1926
            let mut profiler = BrickProfiler::new();
    1927
            profiler.enable();
    1928
    1929
            // Graph disabled by default
    1930
            assert!(!profiler.is_graph_enabled());
    1931
    1932
            profiler.enable_graph();
    1933
            assert!(profiler.is_graph_enabled());
    1934
    1935
            // Push a scope using Layer variant
    1936
            let node = ExecutionNode::Layer { index: 0 };
    1937
            let scope_id = profiler.graph_push_scope(node);
    1938
            assert!(scope_id.is_some());
    1939
    1940
            // Record a brick
    1941
            profiler.graph_record_brick(BrickId::RmsNorm, 1000, 100);
    1942
    1943
            // Pop the scope
    1944
            let popped_id = profiler.graph_pop_scope();
    1945
            assert!(popped_id.is_some());
    1946
    1947
            assert!(profiler.graph_is_scope_balanced());
    1948
    1949
            profiler.disable_graph();
    1950
            assert!(!profiler.is_graph_enabled());
    1951
        }
    1952
    1953
        #[test]
    1954
        fn test_brick_profiler_graph_to_dot() {
    1955
            let mut profiler = BrickProfiler::new();
    1956
            profiler.enable();
    1957
            profiler.enable_graph();
    1958
    1959
            let node = ExecutionNode::Layer { index: 1 };
    1960
            profiler.graph_push_scope(node);
    1961
            profiler.graph_record_brick(BrickId::RmsNorm, 500, 50);
    1962
            profiler.graph_pop_scope();
    1963
    1964
            let dot = profiler.graph_to_dot();
    1965
            assert!(dot.contains("digraph"));
    1966
        }
    1967
    1968
        #[test]
    1969
        fn test_brick_profiler_graph_clear() {
    1970
            let mut profiler = BrickProfiler::new();
    1971
            profiler.enable();
    1972
            profiler.enable_graph();
    1973
    1974
            let node = ExecutionNode::Layer { index: 2 };
    1975
            profiler.graph_push_scope(node);
    1976
            profiler.graph_pop_scope();
    1977
    1978
            profiler.graph_clear();
    1979
            // Should be balanced after clear
    1980
            assert!(profiler.graph_is_scope_balanced());
    1981
        }
    1982
    1983
        #[test]
    1984
        fn test_brick_profiler_l2_cache_hit_rate() {
    1985
            let mut profiler = BrickProfiler::new();
    1986
    1987
            // Default is None
    1988
            assert!(profiler.l2_cache_hit_rate().is_none());
    1989
    1990
            profiler.set_l2_cache_hit_rate(0.95);
    1991
            assert_eq!(profiler.l2_cache_hit_rate(), Some(0.95));
    1992
        }
    1993
    1994
        #[test]
    1995
        fn test_brick_profiler_zero_copy() {
    1996
            let mut profiler = BrickProfiler::new();
    1997
    1998
            // Default is false
    1999
            assert!(!profiler.is_zero_copy());
    2000
    2001
            profiler.set_zero_copy(true);
    2002
            assert!(profiler.is_zero_copy());
    2003
    2004
            profiler.set_zero_copy(false);
    2005
            assert!(!profiler.is_zero_copy());
    2006
        }
    2007
    2008
        #[test]
    2009
        fn test_brick_profiler_reset_epoch() {
    2010
            let mut profiler = BrickProfiler::new();
    2011
            profiler.enable();
    2012
    2013
            // Let some time pass
    2014
            std::thread::sleep(std::time::Duration::from_micros(100));
    2015
            let ns1 = profiler.elapsed_ns();
    2016
            assert!(ns1 > 0);
    2017
    2018
            profiler.reset_epoch();
    2019
            let ns2 = profiler.elapsed_ns();
    2020
            // After reset, elapsed should be close to zero
    2021
            assert!(ns2 < ns1);
    2022
        }
    2023
    2024
        #[test]
    2025
        fn test_brick_profiler_brick_stats_mut() {
    2026
            let mut profiler = BrickProfiler::new();
    2027
            profiler.enable();
    2028
    2029
            // Get mutable stats and modify
    2030
            let stats = profiler.brick_stats_mut(BrickId::RmsNorm);
    2031
            stats.count = 42;
    2032
    2033
            // Verify modification persisted
    2034
            let stats = profiler.brick_stats(BrickId::RmsNorm);
    2035
            assert_eq!(stats.count, 42);
    2036
        }
    2037
    2038
        #[test]
    2039
        fn test_brick_profiler_record_deferred_dynamic() {
    2040
            let mut profiler = BrickProfiler::new();
    2041
            profiler.enable();
    2042
    2043
            let start_ns = profiler.elapsed_ns();
    2044
            std::thread::sleep(std::time::Duration::from_micros(50));
    2045
            profiler.record_deferred_dynamic("DynamicBrick", start_ns, 75);
    2046
    2047
            assert!(profiler.has_pending());
    2048
            assert_eq!(profiler.pending_count(), 1);
    2049
    2050
            let end_ns = profiler.elapsed_ns();
    2051
            profiler.finalize(end_ns);
    2052
    2053
            assert!(!profiler.has_pending());
    2054
        }
    2055
    2056
        #[test]
    2057
        fn test_tile_stats_default() {
    2058
            let stats = TileStats::default();
    2059
            assert_eq!(stats.level, TileLevel::Macro); // Default is Macro
    2060
            assert_eq!(stats.count, 0);
    2061
        }
    2062
    2063
        #[test]
    2064
        fn test_tile_level_default() {
    2065
            let level = TileLevel::default();
    2066
            assert_eq!(level, TileLevel::Macro);
    2067
        }
    2068
    2069
        #[test]
    2070
        fn test_brick_profiler_execution_graph_accessors() {
    2071
            let mut profiler = BrickProfiler::new();
    2072
    2073
            // Read-only access
    2074
            let _graph = profiler.execution_graph();
    2075
    2076
            // Mutable access
    2077
            let _graph_mut = profiler.execution_graph_mut();
    2078
        }
    2079
    2080
        #[test]
    2081
        fn test_brick_profiler_graph_record_kernel() {
    2082
            let mut profiler = BrickProfiler::new();
    2083
            profiler.enable();
    2084
            profiler.enable_graph();
    2085
    2086
            let node = ExecutionNode::Layer { index: 3 };
    2087
            profiler.graph_push_scope(node);
    2088
            // graph_record_kernel(name, ptx_hash, grid, block, shared_mem)
    2089
            profiler.graph_record_kernel("my_kernel", 0x12345678, (1, 1, 1), (256, 1, 1), 4096);
    2090
            profiler.graph_pop_scope();
    2091
    2092
            // Should be able to convert to DOT
    2093
            let dot = profiler.graph_to_dot();
    2094
            assert!(!dot.is_empty());
    2095
        }
    2096
    2097
        #[test]
    2098
        fn test_brick_profiler_graph_disabled_operations() {
    2099
            let mut profiler = BrickProfiler::new();
    2100
            profiler.enable();
    2101
            // Graph is NOT enabled
    2102
    2103
            // Operations should be no-ops when graph disabled
    2104
            let node = ExecutionNode::Layer { index: 4 };
    2105
            let scope_id = profiler.graph_push_scope(node);
    2106
            assert!(scope_id.is_none()); // Returns None when disabled
    2107
    2108
            let popped = profiler.graph_pop_scope();
    2109
            assert!(popped.is_none());
    2110
        }
    2111
    }