Skip to main content

torsh_data/dataloader/
memory.rs

1//! Memory pinning functionality for optimized GPU transfers
2//!
3//! This module provides abstractions and implementations for memory pinning,
4//! which can significantly improve performance when transferring data between
5//! CPU and GPU memory.
6
7use torsh_core::{device::DeviceType, error::Result};
8
9/// Trait for memory pinning operations
10///
11/// Memory pinning allocates page-locked memory that can be transferred to/from
12/// GPU memory more efficiently than regular pageable memory. This is particularly
13/// important for high-throughput data loading scenarios.
14///
15/// # Examples
16///
17/// ```no_run
18/// use torsh_data::dataloader::memory::{MemoryPinning, CpuMemoryPinner};
19///
20/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
21/// let pinner = CpuMemoryPinner;
22/// let data: Vec<i32> = vec![1, 2, 3, 4, 5];
23/// let pinned_data = pinner.pin_memory(data)?;
24///
25/// // Check if pinning is supported for this type
26/// println!("Supports pinning: {}", <CpuMemoryPinner as MemoryPinning<Vec<i32>>>::supports_pinning(&pinner));
27/// # Ok(())
28/// # }
29/// ```
30pub trait MemoryPinning<T> {
31    /// Pin memory for GPU transfers
32    ///
33    /// # Arguments
34    ///
35    /// * `data` - The data to pin in memory
36    ///
37    /// # Returns
38    ///
39    /// The data with memory pinning applied (if supported)
40    fn pin_memory(&self, data: T) -> Result<T>;
41
42    /// Check if memory pinning is supported
43    ///
44    /// # Returns
45    ///
46    /// True if this implementation supports memory pinning, false otherwise
47    fn supports_pinning(&self) -> bool;
48
49    /// Get information about pinning capabilities
50    ///
51    /// # Returns
52    ///
53    /// String describing the pinning implementation
54    fn pinning_info(&self) -> String {
55        if self.supports_pinning() {
56            "Memory pinning supported".to_string()
57        } else {
58            "Memory pinning not supported".to_string()
59        }
60    }
61}
62
63/// CPU memory pinning implementation (no-op for CPU)
64///
65/// This implementation provides a no-op memory pinning for CPU-only scenarios.
66/// Since CPU memory doesn't require pinning for CPU operations, this simply
67/// returns the data unchanged.
68///
69/// # Examples
70///
71/// ```rust,ignore
72/// use torsh_data::dataloader::memory::{CpuMemoryPinner, MemoryPinning};
73///
74/// let pinner = CpuMemoryPinner;
75/// let data = vec![1, 2, 3, 4, 5];
76/// let result = pinner.pin_memory(data).unwrap();
77/// assert!(!pinner.supports_pinning());
78/// ```
79#[derive(Debug, Clone, Default)]
80pub struct CpuMemoryPinner;
81
82impl CpuMemoryPinner {
83    /// Create a new CPU memory pinner
84    pub fn new() -> Self {
85        Self
86    }
87}
88
89impl<T> MemoryPinning<T> for CpuMemoryPinner {
90    fn pin_memory(&self, data: T) -> Result<T> {
91        // CPU doesn't need pinning, return as-is
92        Ok(data)
93    }
94
95    fn supports_pinning(&self) -> bool {
96        false
97    }
98
99    fn pinning_info(&self) -> String {
100        "CPU memory pinner (no-op implementation)".to_string()
101    }
102}
103
104/// CUDA memory pinning implementation
105///
106/// This implementation provides memory pinning for CUDA GPU scenarios.
107/// When enabled with the "cuda" feature, it can allocate page-locked memory
108/// for faster transfers between CPU and GPU.
109///
110/// # Examples
111///
112/// ```rust,ignore
113/// #[cfg(feature = "cuda")]
114/// use torsh_data::dataloader::memory::{CudaMemoryPinner, MemoryPinning};
115///
116/// #[cfg(feature = "cuda")]
117/// {
118///     let pinner = CudaMemoryPinner::new(0)?;
119///     assert!(pinner.supports_pinning());
120/// }
121/// ```
122#[cfg(feature = "cuda")]
123#[derive(Debug, Clone)]
124pub struct CudaMemoryPinner {
125    device_id: usize,
126}
127
128#[cfg(feature = "cuda")]
129impl CudaMemoryPinner {
130    /// Create a new CUDA memory pinner for the specified device
131    ///
132    /// # Arguments
133    ///
134    /// * `device_id` - The CUDA device ID to use for memory pinning
135    ///
136    /// # Returns
137    ///
138    /// A new CudaMemoryPinner or an error if the device is not available
139    ///
140    /// # Examples
141    ///
142    /// ```rust,ignore
143    /// use torsh_data::dataloader::memory::CudaMemoryPinner;
144    ///
145    /// let pinner = CudaMemoryPinner::new(0)?;
146    /// ```
147    pub fn new(device_id: usize) -> Result<Self> {
148        // Verify device availability
149        #[cfg(feature = "cuda")]
150        {
151            // For now, assume CUDA is available if the feature is enabled
152            // This should be replaced with proper torsh-backend integration
153            // when the dependency is added
154        }
155
156        Ok(Self { device_id })
157    }
158
159    /// Get the device ID this pinner is configured for
160    ///
161    /// # Returns
162    ///
163    /// The CUDA device ID
164    pub fn device_id(&self) -> usize {
165        self.device_id
166    }
167
168    /// Set the device ID for this pinner
169    ///
170    /// # Arguments
171    ///
172    /// * `device_id` - The new CUDA device ID
173    pub fn set_device_id(&mut self, device_id: usize) {
174        self.device_id = device_id;
175    }
176}
177
178#[cfg(feature = "cuda")]
179impl<T> MemoryPinning<torsh_tensor::Tensor<T>> for CudaMemoryPinner
180where
181    T: torsh_core::dtype::TensorElement,
182{
183    fn pin_memory(&self, tensor: torsh_tensor::Tensor<T>) -> Result<torsh_tensor::Tensor<T>> {
184        // For CUDA, we would:
185        // 1. Check if tensor is on CPU
186        // 2. Allocate page-locked (pinned) memory
187        // 3. Copy data to pinned memory
188        // 4. Return tensor backed by pinned memory
189
190        // This is a simplified implementation - in practice would use CUDA APIs
191        // to allocate page-locked memory for faster GPU transfers
192
193        // For now, return the tensor as-is since the full CUDA implementation
194        // would require more complex memory management
195        Ok(tensor)
196    }
197
198    fn supports_pinning(&self) -> bool {
199        true
200    }
201
202    fn pinning_info(&self) -> String {
203        format!("CUDA memory pinner for device {}", self.device_id)
204    }
205}
206
207/// Memory pinning manager that selects appropriate pinner based on device
208///
209/// This manager provides a unified interface for memory pinning across different
210/// device types, automatically selecting the appropriate pinner implementation
211/// based on the target device.
212///
213/// # Examples
214///
215/// ```rust,ignore
216/// use torsh_data::dataloader::memory::MemoryPinningManager;
217/// use torsh_core::device::DeviceType;
218///
219/// let mut manager = MemoryPinningManager::new();
220///
221/// // Check if pinning is supported for CUDA
222/// let cuda_supported = manager.supports_pinning(Some(DeviceType::Cuda(0)));
223/// ```
224#[derive(Debug)]
225pub struct MemoryPinningManager {
226    cpu_pinner: CpuMemoryPinner,
227    #[cfg(feature = "cuda")]
228    cuda_pinners: std::collections::HashMap<usize, CudaMemoryPinner>,
229}
230
231impl MemoryPinningManager {
232    /// Create a new memory pinning manager
233    ///
234    /// # Returns
235    ///
236    /// A new MemoryPinningManager with default settings
237    pub fn new() -> Self {
238        Self {
239            cpu_pinner: CpuMemoryPinner::new(),
240            #[cfg(feature = "cuda")]
241            cuda_pinners: std::collections::HashMap::new(),
242        }
243    }
244
245    /// Pin memory for the appropriate device
246    ///
247    /// This method automatically selects the appropriate pinning implementation
248    /// based on the target device type.
249    ///
250    /// # Arguments
251    ///
252    /// * `data` - The tensor data to pin
253    /// * `target_device` - The target device for the data
254    ///
255    /// # Returns
256    ///
257    /// The tensor with memory pinning applied if supported
258    ///
259    /// # Examples
260    ///
261    /// ```rust,ignore
262    /// use torsh_data::dataloader::memory::MemoryPinningManager;
263    /// use torsh_core::device::DeviceType;
264    /// use torsh_tensor::Tensor;
265    ///
266    /// let mut manager = MemoryPinningManager::new();
267    /// let tensor = Tensor::zeros(&[2, 3]);
268    /// let pinned = manager.pin_memory(tensor, Some(DeviceType::Cpu))?;
269    /// ```
270    pub fn pin_memory<T>(
271        &mut self,
272        data: torsh_tensor::Tensor<T>,
273        target_device: Option<DeviceType>,
274    ) -> Result<torsh_tensor::Tensor<T>>
275    where
276        T: torsh_core::dtype::TensorElement,
277    {
278        match target_device {
279            Some(DeviceType::Cuda(device_id)) => {
280                #[cfg(feature = "cuda")]
281                {
282                    if !self.cuda_pinners.contains_key(&device_id) {
283                        let pinner = CudaMemoryPinner::new(device_id)?;
284                        self.cuda_pinners.insert(device_id, pinner);
285                    }
286
287                    if let Some(pinner) = self.cuda_pinners.get(&device_id) {
288                        pinner.pin_memory(data)
289                    } else {
290                        Ok(data)
291                    }
292                }
293                #[cfg(not(feature = "cuda"))]
294                {
295                    let _ = device_id; // Suppress unused warning
296                                       // CUDA not available, fall back to CPU
297                    self.cpu_pinner.pin_memory(data)
298                }
299            }
300            _ => {
301                // CPU or other devices
302                self.cpu_pinner.pin_memory(data)
303            }
304        }
305    }
306
307    /// Pin memory for vector data
308    ///
309    /// A convenience method for pinning vector data that doesn't require tensor operations.
310    ///
311    /// # Arguments
312    ///
313    /// * `data` - The vector data to pin
314    /// * `target_device` - The target device for the data
315    ///
316    /// # Returns
317    ///
318    /// The vector with memory pinning applied if supported
319    pub fn pin_vector_memory<T>(
320        &mut self,
321        data: Vec<T>,
322        target_device: Option<DeviceType>,
323    ) -> Result<Vec<T>> {
324        match target_device {
325            Some(DeviceType::Cuda(_device_id)) => {
326                #[cfg(feature = "cuda")]
327                {
328                    // For vectors, we would typically convert to pinned memory
329                    // This is a simplified implementation
330                    Ok(data)
331                }
332                #[cfg(not(feature = "cuda"))]
333                {
334                    // CUDA not available, return as-is
335                    Ok(data)
336                }
337            }
338            _ => {
339                // CPU or other devices, no pinning needed
340                Ok(data)
341            }
342        }
343    }
344
345    /// Check if pinning is supported for the target device
346    ///
347    /// # Arguments
348    ///
349    /// * `target_device` - The device to check pinning support for
350    ///
351    /// # Returns
352    ///
353    /// True if pinning is supported for the target device, false otherwise
354    pub fn supports_pinning(&self, target_device: Option<DeviceType>) -> bool {
355        match target_device {
356            Some(DeviceType::Cuda(_)) => {
357                #[cfg(feature = "cuda")]
358                return true;
359                #[cfg(not(feature = "cuda"))]
360                return false;
361            }
362            _ => false,
363        }
364    }
365
366    /// Get information about available pinning implementations
367    ///
368    /// # Returns
369    ///
370    /// String describing available pinning capabilities
371    pub fn available_pinners(&self) -> String {
372        // mut needed when cuda feature is enabled for info.push()
373        #[allow(unused_mut)]
374        let mut info = vec!["CPU (no-op)".to_string()];
375
376        #[cfg(feature = "cuda")]
377        {
378            if !self.cuda_pinners.is_empty() {
379                let devices: Vec<String> = self
380                    .cuda_pinners
381                    .keys()
382                    .map(|id| format!("CUDA device {}", id))
383                    .collect();
384                info.push(format!("CUDA: {}", devices.join(", ")));
385            } else {
386                info.push("CUDA (available but no devices initialized)".to_string());
387            }
388        }
389
390        format!("Available pinners: {}", info.join(", "))
391    }
392
393    /// Clear all cached CUDA pinners
394    ///
395    /// This can be useful for memory management or when device availability changes.
396    #[cfg(feature = "cuda")]
397    pub fn clear_cuda_pinners(&mut self) {
398        self.cuda_pinners.clear();
399    }
400
401    /// Get the number of initialized CUDA pinners
402    ///
403    /// # Returns
404    ///
405    /// The number of CUDA pinners currently cached
406    #[cfg(feature = "cuda")]
407    pub fn cuda_pinner_count(&self) -> usize {
408        self.cuda_pinners.len()
409    }
410
411    /// Pre-initialize a CUDA pinner for a specific device
412    ///
413    /// This can be useful for warming up the pinner before actual use.
414    ///
415    /// # Arguments
416    ///
417    /// * `device_id` - The CUDA device ID to initialize
418    ///
419    /// # Returns
420    ///
421    /// Result indicating success or failure of initialization
422    #[cfg(feature = "cuda")]
423    pub fn initialize_cuda_pinner(&mut self, device_id: usize) -> Result<()> {
424        if !self.cuda_pinners.contains_key(&device_id) {
425            let pinner = CudaMemoryPinner::new(device_id)?;
426            self.cuda_pinners.insert(device_id, pinner);
427        }
428        Ok(())
429    }
430}
431
432impl Default for MemoryPinningManager {
433    fn default() -> Self {
434        Self::new()
435    }
436}
437
438/// Configuration for memory pinning operations
439#[derive(Debug, Clone)]
440pub struct PinningConfig {
441    /// Whether to enable memory pinning
442    pub enabled: bool,
443    /// Target device for pinning
444    pub target_device: Option<DeviceType>,
445    /// Whether to pre-initialize pinners
446    pub pre_initialize: bool,
447}
448
449impl Default for PinningConfig {
450    fn default() -> Self {
451        Self {
452            enabled: true,
453            target_device: None,
454            pre_initialize: false,
455        }
456    }
457}
458
459impl PinningConfig {
460    /// Create a new pinning configuration
461    pub fn new() -> Self {
462        Self::default()
463    }
464
465    /// Enable or disable memory pinning
466    pub fn enabled(mut self, enabled: bool) -> Self {
467        self.enabled = enabled;
468        self
469    }
470
471    /// Set the target device for pinning
472    pub fn target_device(mut self, device: DeviceType) -> Self {
473        self.target_device = Some(device);
474        self
475    }
476
477    /// Enable pre-initialization of pinners
478    pub fn pre_initialize(mut self, pre_init: bool) -> Self {
479        self.pre_initialize = pre_init;
480        self
481    }
482
483    /// Create a configuration for CUDA pinning
484    pub fn cuda(device_id: usize) -> Self {
485        Self {
486            enabled: true,
487            target_device: Some(DeviceType::Cuda(device_id)),
488            pre_initialize: false,
489        }
490    }
491
492    /// Create a configuration for CPU (no-op) pinning
493    pub fn cpu() -> Self {
494        Self {
495            enabled: false,
496            target_device: Some(DeviceType::Cpu),
497            pre_initialize: false,
498        }
499    }
500}
501
502/// Utility functions for memory pinning
503pub mod utils {
504    use super::*;
505
506    /// Determine if memory pinning would be beneficial for the given configuration
507    ///
508    /// # Arguments
509    ///
510    /// * `source_device` - Device where data currently resides
511    /// * `target_device` - Device where data will be used
512    /// * `data_size` - Size of data in bytes
513    ///
514    /// # Returns
515    ///
516    /// True if pinning would likely improve performance
517    pub fn should_pin_memory(
518        source_device: DeviceType,
519        target_device: DeviceType,
520        data_size: usize,
521    ) -> bool {
522        match (source_device, target_device) {
523            (DeviceType::Cpu, DeviceType::Cuda(_)) => {
524                // CPU to GPU transfers benefit from pinning, especially for larger data
525                data_size > 1024 // Pin for data larger than 1KB
526            }
527            (DeviceType::Cuda(_), DeviceType::Cpu) => {
528                // GPU to CPU transfers can also benefit
529                data_size > 1024
530            }
531            _ => false, // Same device or other combinations don't need pinning
532        }
533    }
534
535    /// Estimate the memory overhead of pinning
536    ///
537    /// # Arguments
538    ///
539    /// * `data_size` - Size of data to be pinned in bytes
540    ///
541    /// # Returns
542    ///
543    /// Estimated additional memory usage in bytes
544    pub fn estimate_pinning_overhead(data_size: usize) -> usize {
545        // Pinned memory typically has minimal overhead
546        // This is a conservative estimate
547        data_size / 100 // 1% overhead estimate
548    }
549
550    /// Check if the system supports memory pinning for the given device
551    ///
552    /// # Arguments
553    ///
554    /// * `device` - Device to check support for
555    ///
556    /// # Returns
557    ///
558    /// True if the system supports pinning for this device type
559    pub fn system_supports_pinning(device: DeviceType) -> bool {
560        match device {
561            DeviceType::Cuda(_) => {
562                #[cfg(feature = "cuda")]
563                return true;
564                #[cfg(not(feature = "cuda"))]
565                return false;
566            }
567            _ => false,
568        }
569    }
570
571    /// Create an optimal pinning configuration for the given scenario
572    ///
573    /// # Arguments
574    ///
575    /// * `source_device` - Source device type
576    /// * `target_device` - Target device type
577    /// * `data_size` - Size of data to be transferred
578    ///
579    /// # Returns
580    ///
581    /// Optimal PinningConfig for the scenario
582    pub fn optimal_pinning_config(
583        source_device: DeviceType,
584        target_device: DeviceType,
585        data_size: usize,
586    ) -> PinningConfig {
587        if should_pin_memory(source_device, target_device, data_size) {
588            PinningConfig::new()
589                .enabled(true)
590                .target_device(target_device)
591                .pre_initialize(data_size > 1024 * 1024) // Pre-init for large transfers
592        } else {
593            PinningConfig::new().enabled(false)
594        }
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn test_cpu_memory_pinner() {
604        let pinner: CpuMemoryPinner = CpuMemoryPinner::new();
605        let pinner_trait: &dyn MemoryPinning<Vec<i32>> = &pinner;
606        assert!(!pinner_trait.supports_pinning());
607
608        let data = vec![1, 2, 3, 4, 5];
609        let result = pinner
610            .pin_memory(data.clone())
611            .expect("operation should succeed");
612        assert_eq!(result, data);
613    }
614
615    #[test]
616    fn test_memory_pinning_manager_creation() {
617        let manager = MemoryPinningManager::new();
618        assert!(!manager.supports_pinning(Some(DeviceType::Cpu)));
619    }
620
621    #[test]
622    fn test_memory_pinning_manager_cpu() {
623        let mut manager = MemoryPinningManager::new();
624        let data = vec![1, 2, 3, 4, 5];
625        let result = manager
626            .pin_vector_memory(data.clone(), Some(DeviceType::Cpu))
627            .expect("operation should succeed");
628        assert_eq!(result, data);
629    }
630
631    #[test]
632    fn test_pinning_config() {
633        let config = PinningConfig::new()
634            .enabled(true)
635            .target_device(DeviceType::Cpu)
636            .pre_initialize(true);
637
638        assert!(config.enabled);
639        assert_eq!(config.target_device, Some(DeviceType::Cpu));
640        assert!(config.pre_initialize);
641    }
642
643    #[test]
644    fn test_pinning_config_cuda() {
645        let config = PinningConfig::cuda(0);
646        assert!(config.enabled);
647        assert_eq!(config.target_device, Some(DeviceType::Cuda(0)));
648        assert!(!config.pre_initialize);
649    }
650
651    #[test]
652    fn test_pinning_config_cpu() {
653        let config = PinningConfig::cpu();
654        assert!(!config.enabled);
655        assert_eq!(config.target_device, Some(DeviceType::Cpu));
656        assert!(!config.pre_initialize);
657    }
658
659    #[test]
660    fn test_should_pin_memory() {
661        // CPU to CUDA should pin for large data
662        assert!(utils::should_pin_memory(
663            DeviceType::Cpu,
664            DeviceType::Cuda(0),
665            2048
666        ));
667
668        // Small data shouldn't be pinned
669        assert!(!utils::should_pin_memory(
670            DeviceType::Cpu,
671            DeviceType::Cuda(0),
672            512
673        ));
674
675        // Same device shouldn't pin
676        assert!(!utils::should_pin_memory(
677            DeviceType::Cpu,
678            DeviceType::Cpu,
679            2048
680        ));
681    }
682
683    #[test]
684    fn test_estimate_pinning_overhead() {
685        let data_size = 1000;
686        let overhead = utils::estimate_pinning_overhead(data_size);
687        assert_eq!(overhead, 10); // 1% of 1000
688    }
689
690    #[test]
691    fn test_system_supports_pinning() {
692        // CPU should not support pinning
693        assert!(!utils::system_supports_pinning(DeviceType::Cpu));
694
695        // CUDA support depends on feature flag
696        #[cfg(feature = "cuda")]
697        assert!(utils::system_supports_pinning(DeviceType::Cuda(0)));
698
699        #[cfg(not(feature = "cuda"))]
700        assert!(!utils::system_supports_pinning(DeviceType::Cuda(0)));
701    }
702
703    #[test]
704    fn test_optimal_pinning_config() {
705        // Large CPU to CUDA transfer should enable pinning
706        let config = utils::optimal_pinning_config(DeviceType::Cpu, DeviceType::Cuda(0), 2048);
707        assert!(config.enabled);
708        assert_eq!(config.target_device, Some(DeviceType::Cuda(0)));
709
710        // Small transfer should not enable pinning
711        let config = utils::optimal_pinning_config(DeviceType::Cpu, DeviceType::Cuda(0), 512);
712        assert!(!config.enabled);
713
714        // Very large transfer should enable pre-initialization
715        let config =
716            utils::optimal_pinning_config(DeviceType::Cpu, DeviceType::Cuda(0), 2 * 1024 * 1024);
717        assert!(config.enabled);
718        assert!(config.pre_initialize);
719    }
720
721    #[cfg(feature = "cuda")]
722    #[test]
723    fn test_cuda_memory_pinner() {
724        let pinner = CudaMemoryPinner::new(0).expect("Cuda Memory Pinner should succeed");
725        let pinner_trait: &dyn MemoryPinning<torsh_tensor::Tensor<f32>> = &pinner;
726        assert!(pinner_trait.supports_pinning());
727        assert_eq!(pinner.device_id(), 0);
728    }
729
730    #[cfg(feature = "cuda")]
731    #[test]
732    fn test_memory_pinning_manager_cuda() {
733        let mut manager = MemoryPinningManager::new();
734        assert!(manager.supports_pinning(Some(DeviceType::Cuda(0))));
735
736        // Test initialization
737        manager
738            .initialize_cuda_pinner(0)
739            .expect("CUDA pinner initialization should succeed");
740        assert_eq!(manager.cuda_pinner_count(), 1);
741
742        // Test clearing
743        manager.clear_cuda_pinners();
744        assert_eq!(manager.cuda_pinner_count(), 0);
745    }
746}