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
}