Skip to main content

scirs2_linalg/gpu/
memory.rs

1//! GPU memory management utilities
2
3use super::{GpuBuffer, GpuContext, GpuContextAlloc};
4use crate::error::LinalgResult;
5
6/// Memory allocation strategies for GPU operations
7#[derive(Debug, Clone, Copy)]
8pub enum MemoryStrategy {
9    /// Allocate memory as needed
10    OnDemand,
11    /// Pre-allocate memory pools
12    Pooled,
13    /// Use unified/managed memory where available
14    Unified,
15    /// Use pinned host memory for faster transfers
16    Pinned,
17}
18
19/// Memory pool for reusing GPU allocations
20pub struct MemoryPool<T> {
21    buffers: Vec<Box<dyn GpuBuffer<T>>>,
22    max_poolsize: usize,
23    total_allocated: usize,
24}
25
26impl<T: Clone + Send + Sync + Copy + 'static + std::fmt::Debug> MemoryPool<T> {
27    /// Create a new memory pool
28    pub fn new(max_poolsize: usize) -> Self {
29        Self {
30            buffers: Vec::new(),
31            max_poolsize,
32            total_allocated: 0,
33        }
34    }
35
36    /// Get a buffer from the pool or allocate a new one
37    pub fn get_buffer<C: GpuContextAlloc>(
38        &mut self,
39        context: &C,
40        size: usize,
41    ) -> LinalgResult<Box<dyn GpuBuffer<T>>> {
42        // Try to find a buffer of suitable size in the pool
43        for i in 0..self.buffers.len() {
44            if self.buffers[i].len() >= size {
45                return Ok(self.buffers.swap_remove(i));
46            }
47        }
48
49        // No suitable buffer found, allocate a new one
50        let buffer = context.allocate_buffer(size)?;
51        self.total_allocated += size;
52        Ok(buffer)
53    }
54
55    /// Return a buffer to the pool
56    pub fn return_buffer(&mut self, buffer: Box<dyn GpuBuffer<T>>) {
57        if self.buffers.len() < self.max_poolsize {
58            self.buffers.push(buffer);
59        }
60        // If pool is full, buffer will be dropped
61    }
62
63    /// Clear all buffers from the pool
64    pub fn clear(&mut self) {
65        self.buffers.clear();
66        self.total_allocated = 0;
67    }
68
69    /// Get total allocated memory in elements
70    pub fn total_allocated(&self) -> usize {
71        self.total_allocated
72    }
73
74    /// Get number of buffers in pool
75    pub fn poolsize(&self) -> usize {
76        self.buffers.len()
77    }
78}
79
80/// Memory transfer operations between host and device
81pub trait MemoryTransfer<T> {
82    /// Copy data from host to device asynchronously
83    fn copy_host_to_device_async(
84        &self,
85        host_data: &[T],
86        device_buffer: &mut dyn GpuBuffer<T>,
87    ) -> LinalgResult<()>;
88
89    /// Copy data from device to host asynchronously
90    fn copy_device_to_host_async(
91        &self,
92        device_buffer: &dyn GpuBuffer<T>,
93        host_data: &mut [T],
94    ) -> LinalgResult<()>;
95
96    /// Copy data between device buffers
97    fn copy_device_to_device(
98        &self,
99        src_buffer: &dyn GpuBuffer<T>,
100        dst_buffer: &mut dyn GpuBuffer<T>,
101    ) -> LinalgResult<()>;
102}
103
104/// Memory bandwidth measurement and optimization
105pub struct MemoryBandwidthProfiler {
106    measurements: Vec<f64>, // GB/s measurements
107}
108
109impl Default for MemoryBandwidthProfiler {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl MemoryBandwidthProfiler {
116    /// Create a new bandwidth profiler
117    pub fn new() -> Self {
118        Self {
119            measurements: Vec::new(),
120        }
121    }
122
123    /// Measure memory bandwidth for a given transfer size
124    pub fn measure_bandwidth<T, C: GpuContextAlloc>(
125        &mut self,
126        context: &C,
127        transfersize: usize,
128    ) -> LinalgResult<f64>
129    where
130        T: Clone + Send + Sync + Default + Copy + 'static + std::fmt::Debug,
131    {
132        let _start_time = std::time::Instant::now();
133
134        // Allocate buffers
135        let mut buffer1 = context.allocate_buffer::<T>(transfersize)?;
136        let _buffer2 = context.allocate_buffer::<T>(transfersize)?;
137
138        // Create test data
139        let test_data: Vec<T> = (0..transfersize).map(|_| T::default()).collect();
140
141        // Measure host-to-device transfer
142        let h2d_start = std::time::Instant::now();
143        buffer1.copy_from_host(&test_data)?;
144        context.synchronize()?;
145        let h2d_time = h2d_start.elapsed().as_secs_f64();
146
147        // Measure device-to-host transfer
148        let mut result_data = vec![T::default(); transfersize];
149        let d2h_start = std::time::Instant::now();
150        buffer1.copy_to_host(&mut result_data)?;
151        context.synchronize()?;
152        let d2h_time = d2h_start.elapsed().as_secs_f64();
153
154        // Calculate bandwidth (bytes per second -> GB/s)
155        let bytes_transferred = transfersize * std::mem::size_of::<T>();
156        let total_time = h2d_time + d2h_time;
157        let bandwidth_gb_s = (bytes_transferred as f64 * 2.0) / (total_time * 1e9);
158
159        self.measurements.push(bandwidth_gb_s);
160        Ok(bandwidth_gb_s)
161    }
162
163    /// Get average measured bandwidth
164    pub fn average_bandwidth(&self) -> f64 {
165        if self.measurements.is_empty() {
166            0.0
167        } else {
168            self.measurements.iter().sum::<f64>() / self.measurements.len() as f64
169        }
170    }
171
172    /// Get peak measured bandwidth
173    pub fn peak_bandwidth(&self) -> f64 {
174        self.measurements.iter().copied().fold(0.0, f64::max)
175    }
176}
177
178/// Memory usage tracking and optimization suggestions
179pub struct MemoryOptimizer {
180    usage_history: Vec<usize>,
181    peak_usage: usize,
182}
183
184impl Default for MemoryOptimizer {
185    fn default() -> Self {
186        Self::new()
187    }
188}
189
190impl MemoryOptimizer {
191    /// Create a new memory optimizer
192    pub fn new() -> Self {
193        Self {
194            usage_history: Vec::new(),
195            peak_usage: 0,
196        }
197    }
198
199    /// Record memory usage
200    pub fn record_usage(&mut self, usagebytes: usize) {
201        self.usage_history.push(usagebytes);
202        self.peak_usage = self.peak_usage.max(usagebytes);
203    }
204
205    /// Get optimization suggestions based on usage patterns
206    pub fn get_suggestions(&self, devicememory: usize) -> Vec<String> {
207        let mut suggestions = Vec::new();
208
209        if self.peak_usage > devicememory / 2 {
210            suggestions
211                .push("Consider using _memory pooling to reduce allocation overhead".to_string());
212        }
213
214        if self.usage_history.len() > 10 {
215            let recent_usage: Vec<_> = self.usage_history.iter().rev().take(10).collect();
216            let avg_recent = recent_usage.iter().copied().sum::<usize>() / recent_usage.len();
217
218            if avg_recent < self.peak_usage / 4 {
219                suggestions.push(
220                    "Memory usage varies significantly - consider dynamic allocation".to_string(),
221                );
222            }
223        }
224
225        if self.peak_usage > devicememory * 3 / 4 {
226            suggestions
227                .push("High _memory usage detected - consider out-of-core algorithms".to_string());
228        }
229
230        suggestions
231    }
232
233    /// Calculate memory efficiency score (0-100)
234    pub fn efficiency_score(&self, devicememory: usize) -> f64 {
235        if self.usage_history.is_empty() {
236            return 100.0;
237        }
238
239        let avg_usage = self.usage_history.iter().sum::<usize>() / self.usage_history.len();
240        let utilization = avg_usage as f64 / devicememory as f64;
241
242        // Score based on reasonable utilization (30-70% is good)
243        if utilization < 0.3 {
244            (utilization / 0.3) * 50.0
245        } else if utilization <= 0.7 {
246            100.0
247        } else if utilization <= 1.0 {
248            100.0 - ((utilization - 0.7) / 0.3) * 50.0
249        } else {
250            0.0 // Over 100% utilization
251        }
252    }
253}
254
255/// Check if a given operation fits in GPU memory
256#[allow(dead_code)]
257pub fn check_memory_requirements(
258    context: &dyn GpuContext,
259    matricessizes: &[(usize, usize)],
260    elementsize: usize,
261) -> LinalgResult<bool> {
262    let total_elements: usize = matricessizes.iter().map(|(rows, cols)| rows * cols).sum();
263
264    let total_bytes = total_elements * elementsize;
265    let available_memory = context.available_memory()?;
266
267    // Need some overhead for temporary variables
268    Ok(total_bytes < available_memory / 2)
269}
270
271/// Suggest optimal memory strategy for given problem characteristics
272#[allow(dead_code)]
273pub fn suggest_memory_strategy(
274    problemsize: usize,
275    available_memory: usize,
276    unified_memory_available: bool,
277) -> MemoryStrategy {
278    let memory_ratio = problemsize as f64 / available_memory as f64;
279
280    if memory_ratio > 0.8 {
281        // Large problem relative to _memory
282        if unified_memory_available {
283            MemoryStrategy::Unified
284        } else {
285            MemoryStrategy::OnDemand
286        }
287    } else if memory_ratio < 0.1 {
288        // Small problem
289        MemoryStrategy::Pinned
290    } else {
291        // Medium problem
292        MemoryStrategy::Pooled
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::gpu::backends::CpuFallbackBackend;
300    use crate::gpu::GpuBackend;
301
302    #[test]
303    fn test_memory_pool_operations() {
304        let mut pool = MemoryPool::<f32>::new(5);
305        assert_eq!(pool.poolsize(), 0);
306        assert_eq!(pool.total_allocated(), 0);
307
308        // Can't test actual allocation without a real GPU context
309        pool.clear();
310        assert_eq!(pool.poolsize(), 0);
311    }
312
313    #[test]
314    fn test_memory_optimizer() {
315        let mut optimizer = MemoryOptimizer::new();
316
317        // Record some usage patterns that will trigger suggestions
318        optimizer.record_usage(3000);
319        optimizer.record_usage(6000); // This will be > 5000 (10000/2)
320        optimizer.record_usage(4500);
321
322        let efficiency = optimizer.efficiency_score(10000);
323        assert!((0.0..=100.0).contains(&efficiency));
324
325        let suggestions = optimizer.get_suggestions(10000);
326        // Should have suggestions since peak_usage (6000) > device_memory/2 (5000)
327        assert!(!suggestions.is_empty());
328    }
329
330    #[test]
331    fn test_memory_strategy_suggestions() {
332        // Large problem
333        let strategy = suggest_memory_strategy(1000000, 1000000, true);
334        assert!(matches!(strategy, MemoryStrategy::Unified));
335
336        // Small problem
337        let strategy = suggest_memory_strategy(10000, 1000000, false);
338        assert!(matches!(strategy, MemoryStrategy::Pinned));
339
340        // Medium problem
341        let strategy = suggest_memory_strategy(300000, 1000000, false);
342        assert!(matches!(strategy, MemoryStrategy::Pooled));
343    }
344
345    #[test]
346    fn test_check_memory_requirements() {
347        let backend = CpuFallbackBackend::new();
348        let context = backend.create_context(0).expect("Operation failed");
349
350        // Small matrices should fit
351        let matrices = vec![(10, 10), (10, 10)];
352        let fits =
353            check_memory_requirements(context.as_ref(), &matrices, 8).expect("Operation failed");
354        assert!(fits);
355
356        // Very large matrices might not fit (depends on system memory)
357        let matrices = vec![(100000, 100000)];
358        let fits =
359            check_memory_requirements(context.as_ref(), &matrices, 8).expect("Operation failed");
360        // Result depends on available system memory, just check it doesn't panic
361        let _ = fits;
362    }
363}