## Coverage Report

#### Created: 2026-01-25 15:05

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

    /home/noah/src/realizar/src/gpu/allocator.rs

    Line
    Count
    Source
    1
    //! GPU Memory Allocator Module (PMAT-802)
    2
    //!
    3
    //! Extracted from gpu/mod.rs - Memory pooling and arena allocation for inference.
    4
    //!
    5
    //! ## Contents
    6
    //! - `CacheAlignedBuffer` - Cache-aligned tensor storage (IMP-046)
    7
    //! - `TensorPool` - Buffer reuse pool (IMP-049)
    8
    //! - `ForwardArena` - Bump allocator for forward pass (IMP-050)
    9
    //! - `ScratchBuffer` - Per-layer scratch space (IMP-051)
    10
    //! - Prefetch and blocked matmul utilities
    11
    12
    // ============================================================================
    13
    // Cache Efficiency & Prefetch (M21 - IMP-046/047/048)
    14
    // ============================================================================
    15
    16
    /// Cache line size in bytes (typical x86-64)
    17
    const CACHE_LINE_SIZE: usize = 64;
    18
    19
    /// Cache-aligned buffer for tensor storage (M21 - IMP-046)
    20
    ///
    21
    /// Ensures data is aligned to cache line boundaries (64 bytes) for optimal
    22
    /// memory access patterns and avoiding false sharing.
    23
    #[derive(Debug)]
    24
    pub struct CacheAlignedBuffer {
    25
        /// Underlying storage with extra space for alignment
    26
        data: Vec<f32>,
    27
        /// Offset to aligned start within data
    28
        offset: usize,
    29
        /// Logical length of the buffer
    30
        len: usize,
    31
    }
    32
    33
    impl CacheAlignedBuffer {
    34
        /// Create a new cache-aligned buffer of the given size
    35
        #[must_use]
    36
    15
        pub fn new(len: usize) -> Self {
    37
            // Allocate extra space to ensure we can align
    38
            // 64 bytes = 16 f32 values
    39
    15
            let align_elements = CACHE_LINE_SIZE / std::mem::size_of::<f32>();
    40
    15
            let extra = align_elements - 1;
    41
    15
            let data = vec![0.0f32; len + extra];
    42
    43
            // Find the aligned offset
    44
    15
            let ptr = data.as_ptr() as usize;
    45
    15
            let misalignment = ptr % CACHE_LINE_SIZE;
    46
    15
            let offset = if misalignment == 0 {
    47
    9
                0
    48
            } else {
    49
    6
                (CACHE_LINE_SIZE - misalignment) / std::mem::size_of::<f32>()
    50
            };
    51
    52
    15
            Self { data, offset, len }
    53
    15
        }
    54
    55
        /// Check if the buffer is aligned to the given boundary
    56
        #[must_use]
    57
    9
        pub fn is_aligned(&self, alignment: usize) -> bool {
    58
    9
            let ptr = self.as_slice().as_ptr() as usize;
    59
    9
            ptr.is_multiple_of(alignment)
    60
    9
        }
    61
    62
        /// Get the logical length of the buffer
    63
        #[must_use]
    64
    4
        pub fn len(&self) -> usize {
    65
    4
            self.len
    66
    4
        }
    67
    68
        /// Check if the buffer is empty
    69
        #[must_use]
    70
    3
        pub fn is_empty(&self) -> bool {
    71
    3
            self.len == 0
    72
    3
        }
    73
    74
        /// Get an immutable slice of the aligned data
    75
        #[must_use]
    76
    14
        pub fn as_slice(&self) -> &[f32] {
    77
    14
            &self.data[self.offset..self.offset + self.len]
    78
    14
        }
    79
    80
        /// Get a mutable slice of the aligned data
    81
    4
        pub fn as_mut_slice(&mut self) -> &mut [f32] {
    82
    4
            let offset = self.offset;
    83
    4
            let len = self.len;
    84
    4
            &mut self.data[offset..offset + len]
    85
    4
        }
    86
    }
    87
    88
    /// Software prefetch hint for read access (M21 - IMP-047)
    89
    ///
    90
    /// Hints to the CPU that data at the given position will be needed soon.
    91
    /// This is a no-op on platforms without prefetch support.
    92
    #[inline]
    93
    13.3M
    pub fn prefetch_read(data: &[f32], position: usize, distance: usize) {
    94
    13.3M
        let prefetch_pos = position + distance;
    95
    13.3M
        if prefetch_pos < data.len() {
    96
    13.3M
            // Use a volatile read to hint the prefetch without actual side effects
    97
    13.3M
            // This is a simplified portable approach; real prefetch uses intrinsics
    98
    13.3M
            // SAFETY: We've verified prefetch_pos is in bounds
    99
    13.3M
            let _ = unsafe { std::ptr::read_volatile(&raw const data[prefetch_pos]) };
    100
    13.3M
        }7
    101
    13.3M
    }
    102
    103
    /// Sequential sum without prefetch (baseline for comparison)
    104
    #[must_use]
    105
    210
    pub fn sequential_sum(data: &[f32]) -> f32 {
    106
    210
        data.iter().sum()
    107
    210
    }
    108
    109
    /// Sum with software prefetch hints (M21 - IMP-047)
    110
    ///
    111
    /// Uses prefetch hints to reduce cache miss latency for sequential access.
    112
    #[must_use]
    113
    210
    pub fn sum_with_prefetch(data: &[f32], prefetch_distance: usize) -> f32 {
    114
    210
        let mut sum = 0.0f32;
    115
    210
        let len = data.len();
    116
    117
    13.3M
        for i in 0..len210 {
    118
            // Prefetch ahead
    119
    13.3M
            if i + prefetch_distance < len {
    120
    13.3M
                prefetch_read(data, i, prefetch_distance);
    121
    13.3M
            }13.1k
    122
    13.3M
            sum += data[i];
    123
        }
    124
    125
    210
        sum
    126
    210
    }
    127
    128
    /// Naive matrix multiplication (baseline for comparison)
    129
    ///
    130
    /// Computes C = A @ B where A is (rows x inner) and B is (inner x cols)
    131
    #[must_use]
    132
    24
    pub fn naive_matmul(
    133
    24
        mat_a: &[f32],
    134
    24
        mat_b: &[f32],
    135
    24
        rows: usize,
    136
    24
        inner: usize,
    137
    24
        cols: usize,
    138
    24
    ) -> Vec<f32> {
    139
    24
        let mut result = vec![0.0f32; rows * cols];
    140
    141
    4.49k
        for row in 0..rows24 {
    142
    1.13M
            for col in 0..cols4.49k {
    143
    1.13M
                let mut sum = 0.0f32;
    144
    574M
                for idx in 0..inner1.13M {
    145
    574M
                    sum += mat_a[row * inner + idx] * mat_b[idx * cols + col];
    146
    574M
                }
    147
    1.13M
                result[row * cols + col] = sum;
    148
            }
    149
        }
    150
    151
    24
        result
    152
    24
    }
    153
    154
    /// Cache-blocked matrix multiplication (M21 - IMP-048)
    155
    ///
    156
    /// Uses tiling/blocking to improve cache locality for large matrices.
    157
    /// Block size should be chosen to fit in L1/L2 cache.
    158
    #[must_use]
    159
    #[allow(clippy::many_single_char_names)] // Matrix indices are standard notation
    160
    24
    pub fn blocked_matmul(
    161
    24
        mat_a: &[f32],
    162
    24
        mat_b: &[f32],
    163
    24
        rows: usize,
    164
    24
        inner: usize,
    165
    24
        cols: usize,
    166
    24
        block_size: usize,
    167
    24
    ) -> Vec<f32> {
    168
    24
        let mut result = vec![0.0f32; rows * cols];
    169
    170
        // Process in blocks for better cache utilization
    171
    149
        for row_blk in (0..rows)24.step_by24(block_size24) {
    172
    149
            let row_end = (row_blk + block_size).min(rows);
    173
    174
    1.11k
            for col_blk in (0..cols)149.step_by149(block_size149) {
    175
    1.11k
                let col_end = (col_blk + block_size).min(cols);
    176
    177
    17.5k
                for inner_blk in (0..inner)1.11k.step_by1.11k(block_size1.11k) {
    178
    17.5k
                    let inner_end = (inner_blk + block_size).min(inner);
    179
    180
                    // Inner blocked computation
    181
    561k
                    for row in row_blk17.5k..row_end17.5k {
    182
    17.9M
                        for col in col_blk561k..col_end561k {
    183
    17.9M
                            let mut sum = result[row * cols + col];
    184
    574M
                            for idx in inner_blk17.9M..inner_end17.9M {
    185
    574M
                                sum += mat_a[row * inner + idx] * mat_b[idx * cols + col];
    186
    574M
                            }
    187
    17.9M
                            result[row * cols + col] = sum;
    188
                        }
    189
                    }
    190
                }
    191
            }
    192
        }
    193
    194
    24
        result
    195
    24
    }
    196
    197
    // ============================================================================
    198
    // Phase 13: Memory Pooling & Arena Allocation (M22)
    199
    // ============================================================================
    200
    201
    /// Tensor memory pool for reusing buffers during inference (M22 - IMP-049)
    202
    ///
    203
    /// Maintains a pool of pre-allocated buffers organized by size class
    204
    /// to reduce allocation overhead during token generation.
    205
    #[derive(Debug)]
    206
    pub struct TensorPool {
    207
        /// Maximum number of buffers to keep in pool
    208
        capacity: usize,
    209
        /// Available buffers organized by size
    210
        buffers: Vec<Vec<f32>>,
    211
    }
    212
    213
    impl TensorPool {
    214
        /// Create a new tensor pool with the given capacity
    215
        #[must_use]
    216
    9
        pub fn new(capacity: usize) -> Self {
    217
    9
            Self {
    218
    9
                capacity,
    219
    9
                buffers: Vec::with_capacity(capacity),
    220
    9
            }
    221
    9
        }
    222
    223
        /// Get the maximum capacity of the pool
    224
        #[must_use]
    225
    2
        pub fn capacity(&self) -> usize {
    226
    2
            self.capacity
    227
    2
        }
    228
    229
        /// Get the number of available buffers in the pool
    230
        #[must_use]
    231
    9
        pub fn available(&self) -> usize {
    232
    9
            self.buffers.len()
    233
    9
        }
    234
    235
        /// Acquire a buffer of the given size
    236
        ///
    237
        /// If a suitable buffer exists in the pool, it will be reused.
    238
        /// Otherwise, a new buffer is allocated.
    239
    15
        pub fn acquire(&mut self, size: usize) -> Vec<f32> {
    240
            // Look for a buffer of sufficient size
    241
    15
            if let Some(idx3) = self.buffers.iter().position(|b| b4.capacity4() >= size4) {
    242
    3
                let mut buffer = self.buffers.swap_remove(idx);
    243
    3
                buffer.resize(size, 0.0);
    244
    3
                buffer
    245
            } else {
    246
                // Allocate new buffer
    247
    12
                vec![0.0f32; size]
    248
            }
    249
    15
        }
    250
    251
        /// Release a buffer back to the pool
    252
        ///
    253
        /// The buffer will be kept for reuse if the pool has capacity.
    254
    13
        pub fn release(&mut self, buffer: Vec<f32>) {
    255
    13
            if self.buffers.len() < self.capacity {
    256
    11
                self.buffers.push(buffer);
    257
    11
            }2
    258
            // If at capacity, buffer is dropped
    259
    13
        }
    260
    261
        /// Clear all buffers from the pool
    262
    1
        pub fn clear(&mut self) {
    263
    1
            self.buffers.clear();
    264
    1
        }
    265
    }
    266
    267
    /// Arena allocator for forward pass temporaries (M22 - IMP-050)
    268
    ///
    269
    /// Uses bump allocation for fast, contiguous allocation of tensors
    270
    /// during a single forward pass. Reset between passes for reuse.
    271
    #[derive(Debug)]
    272
    pub struct ForwardArena {
    273
        /// Backing storage
    274
        data: Vec<f32>,
    275
        /// Current allocation offset
    276
        offset: usize,
    277
    }
    278
    279
    impl ForwardArena {
    280
        /// Create a new arena with the given capacity (in f32 elements)
    281
        #[must_use]
    282
    6
        pub fn new(capacity: usize) -> Self {
    283
    6
            Self {
    284
    6
                data: vec![0.0f32; capacity],
    285
    6
                offset: 0,
    286
    6
            }
    287
    6
        }
    288
    289
        /// Get the total capacity of the arena
    290
        #[must_use]
    291
    2
        pub fn capacity(&self) -> usize {
    292
    2
            self.data.len()
    293
    2
        }
    294
    295
        /// Get the current used amount
    296
        #[must_use]
    297
    7
        pub fn used(&self) -> usize {
    298
    7
            self.offset
    299
    7
        }
    300
    301
        /// Allocate a slice of the given size from the arena
    302
        ///
    303
        /// Returns a mutable slice into the arena's backing storage.
    304
        /// Panics if there is insufficient capacity.
    305
    10
        pub fn alloc(&mut self, size: usize) -> &mut [f32] {
    306
    10
            let start = self.offset;
    307
    10
            let end = start + size;
    308
    309
    10
            assert!(
    310
    10
                end <= self.data.len(),
    311
    0
                "ForwardArena: insufficient capacity (need {}, have {})",
    312
                end,
    313
    0
                self.data.len()
    314
            );
    315
    316
    10
            self.offset = end;
    317
    10
            &mut self.data[start..end]
    318
    10
        }
    319
    320
        /// Reset the arena for reuse
    321
        ///
    322
        /// This does not deallocate memory, just resets the offset.
    323
    3
        pub fn reset(&mut self) {
    324
    3
            self.offset = 0;
    325
    3
        }
    326
    }
    327
    328
    /// Scratch buffer for layer-wise intermediate computations (M22 - IMP-051)
    329
    ///
    330
    /// Provides pre-allocated scratch space for each transformer layer,
    331
    /// avoiding repeated allocations during inference.
    332
    #[derive(Debug)]
    333
    pub struct ScratchBuffer {
    334
        /// Number of layers
    335
        num_layers: usize,
    336
        /// Size per layer (in f32 elements)
    337
        layer_size: usize,
    338
        /// Backing storage (contiguous for all layers)
    339
        data: Vec<f32>,
    340
    }
    341
    342
    impl ScratchBuffer {
    343
        /// Create scratch buffers for the given number of layers
    344
        #[must_use]
    345
    7
        pub fn new(num_layers: usize, layer_size: usize) -> Self {
    346
    7
            Self {
    347
    7
                num_layers,
    348
    7
                layer_size,
    349
    7
                data: vec![0.0f32; num_layers * layer_size],
    350
    7
            }
    351
    7
        }
    352
    353
        /// Get the number of layers
    354
        #[must_use]
    355
    3
        pub fn num_layers(&self) -> usize {
    356
    3
            self.num_layers
    357
    3
        }
    358
    359
        /// Get the size per layer
    360
        #[must_use]
    361
    3
        pub fn layer_size(&self) -> usize {
    362
    3
            self.layer_size
    363
    3
        }
    364
    365
        /// Get the total size of all scratch buffers
    366
        #[must_use]
    367
    3
        pub fn total_size(&self) -> usize {
    368
    3
            self.num_layers * self.layer_size
    369
    3
        }
    370
    371
        /// Get immutable scratch space for a specific layer
    372
        ///
    373
        /// # Panics
    374
        /// Panics if layer_idx >= num_layers
    375
        #[must_use]
    376
    17
        pub fn get_layer(&self, layer_idx: usize) -> &[f32] {
    377
    17
            assert!(
    378
    17
                layer_idx < self.num_layers,
    379
    0
                "ScratchBuffer: layer index {} out of bounds (max {})",
    380
                layer_idx,
    381
                self.num_layers
    382
            );
    383
    17
            let start = layer_idx * self.layer_size;
    384
    17
            let end = start + self.layer_size;
    385
    17
            &self.data[start..end]
    386
    17
        }
    387
    388
        /// Get mutable scratch space for a specific layer
    389
        ///
    390
        /// # Panics
    391
        /// Panics if layer_idx >= num_layers
    392
    8
        pub fn get_layer_mut(&mut self, layer_idx: usize) -> &mut [f32] {
    393
    8
            assert!(
    394
    8
                layer_idx < self.num_layers,
    395
    0
                "ScratchBuffer: layer index {} out of bounds (max {})",
    396
                layer_idx,
    397
                self.num_layers
    398
            );
    399
    8
            let start = layer_idx * self.layer_size;
    400
    8
            let end = start + self.layer_size;
    401
    8
            &mut self.data[start..end]
    402
    8
        }
    403
    404
        /// Reset all scratch buffers to zero
    405
    3
        pub fn reset(&mut self) {
    406
    3
            self.data.fill(0.0);
    407
    3
        }
    408
    }