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}