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