Skip to main content

torsh_data/dataloader/
mod.rs

1//! DataLoader implementation for efficient data loading
2//!
3//! This module provides comprehensive data loading capabilities with support for
4//! batching, parallel processing, prefetching, and memory optimization.
5//!
6//! # Overview
7//!
8//! The DataLoader system consists of several key components:
9//!
10//! - **Core**: Fundamental DataLoader functionality including the main struct, iterator, and builder
11//! - **Simple**: Convenience functions for quick DataLoader creation
12//! - **Prefetch**: Performance optimization through background data loading
13//! - **Workers**: Multi-process data loading with worker pools
14//! - **Memory**: Memory pinning for optimized GPU transfers
15//!
16//! # Quick Start
17//!
18//! ```rust,ignore
19//! use torsh_data::dataloader::{DataLoader, simple_dataloader};
20//! use torsh_data::dataset::TensorDataset;
21//!
22//! // Using the builder pattern
23//! let dataset = TensorDataset::new(vec![1, 2, 3, 4, 5]);
24//! let dataloader = DataLoader::builder(dataset)
25//!     .batch_size(2)
26//!     .num_workers(4)
27//!     .shuffle(true)
28//!     .build()?;
29//!
30//! for batch in dataloader.iter() {
31//!     // Process batch
32//! }
33//!
34//! // Using the simple API
35//! let dataset = TensorDataset::new(vec![1, 2, 3, 4, 5]);
36//! let dataloader = simple_dataloader(dataset, 2, false)?;
37//! ```
38//!
39//! # Advanced Usage
40//!
41//! ## Prefetching for Performance
42//!
43//! ```rust,ignore
44//! use torsh_data::dataloader::{DataLoader, prefetch::PrefetchExt};
45//! use torsh_data::dataset::TensorDataset;
46//!
47//! let dataset = TensorDataset::new(vec![1, 2, 3, 4, 5]);
48//! let dataloader = DataLoader::builder(dataset).batch_size(2).build()?;
49//!
50//! // Add prefetching for better performance
51//! let prefetch_iter = dataloader.iter().prefetch(2);
52//! for batch in prefetch_iter {
53//!     // Process batch while next batches are being prefetched
54//! }
55//! ```
56//!
57//! ## Multi-Process Loading
58//!
59//! ```rust,ignore
60//! use std::sync::Arc;
61//! use torsh_data::dataloader::{DataLoader, workers::WorkerPool};
62//! use torsh_data::dataset::TensorDataset;
63//! use torsh_data::collate::DefaultCollate;
64//!
65//! let dataset = Arc::new(TensorDataset::new(vec![1, 2, 3, 4, 5]));
66//! let collate_fn = Arc::new(DefaultCollate);
67//! let worker_pool = WorkerPool::new(dataset, collate_fn, 4);
68//!
69//! // Submit tasks and collect results
70//! worker_pool.submit_task(0, vec![0, 1])?;
71//! let result = worker_pool.get_result()?;
72//! ```
73//!
74//! ## Memory Pinning for GPU
75//!
76//! ```rust,ignore
77//! use torsh_data::dataloader::memory::{MemoryPinningManager, PinningConfig};
78//! use torsh_core::device::DeviceType;
79//!
80//! let mut manager = MemoryPinningManager::new();
81//! let config = PinningConfig::cuda(0);
82//!
83//! // Pin memory for faster GPU transfers
84//! if manager.supports_pinning(Some(DeviceType::Cuda(0))) {
85//!     // Use pinned memory for data transfers
86//! }
87//! ```
88
89use torsh_core::error::Result;
90
91// Re-export sub-modules
92pub mod core;
93pub mod memory;
94pub mod prefetch;
95pub mod simple;
96pub mod workers;
97
98// Re-export core types for backward compatibility and convenience
99pub use core::{
100    DataLoader, DataLoaderBuilder, DataLoaderIterator, DataLoaderTrait, RandomDataLoader,
101    SimpleDataLoader,
102};
103
104// Re-export simple API functions
105pub use simple::{
106    simple_dataloader, simple_random_dataloader, SimpleConfig, SimpleRandomDataLoader,
107};
108
109// Re-export prefetch functionality
110pub use prefetch::{PrefetchConfig, PrefetchExt, PrefetchIterator};
111
112// Re-export worker functionality
113pub use workers::{
114    MultiProcessIterator, PersistentWorkerPool, WorkerConfig, WorkerPool, WorkerResult,
115};
116
117// Re-export memory functionality
118pub use memory::{CpuMemoryPinner, MemoryPinning, MemoryPinningManager, PinningConfig};
119
120#[cfg(feature = "cuda")]
121pub use memory::CudaMemoryPinner;
122
123/// Configuration for DataLoader creation with all options
124///
125/// This struct provides a comprehensive configuration interface that combines
126/// options from all DataLoader components.
127#[derive(Debug, Clone)]
128pub struct DataLoaderConfig {
129    /// Batch size for data loading
130    pub batch_size: usize,
131    /// Whether to shuffle the data
132    pub shuffle: bool,
133    /// Number of worker threads
134    pub num_workers: usize,
135    /// Whether to pin memory for GPU transfers
136    pub pin_memory: bool,
137    /// Whether to drop the last incomplete batch
138    pub drop_last: bool,
139    /// Timeout for batch collection
140    pub timeout: Option<std::time::Duration>,
141    /// Random seed for reproducible shuffling
142    pub generator: Option<u64>,
143    /// Prefetch buffer size (0 to disable prefetching)
144    pub prefetch_buffer_size: usize,
145    /// Whether to use persistent workers
146    pub persistent_workers: bool,
147    /// Memory pinning configuration
148    pub pinning_config: Option<PinningConfig>,
149}
150
151impl Default for DataLoaderConfig {
152    fn default() -> Self {
153        Self {
154            batch_size: 1,
155            shuffle: false,
156            num_workers: 0,
157            pin_memory: false,
158            drop_last: false,
159            timeout: None,
160            generator: None,
161            prefetch_buffer_size: 0,
162            persistent_workers: false,
163            pinning_config: None,
164        }
165    }
166}
167
168impl DataLoaderConfig {
169    /// Create a new DataLoader configuration
170    pub fn new() -> Self {
171        Self::default()
172    }
173
174    /// Set batch size
175    pub fn batch_size(mut self, batch_size: usize) -> Self {
176        self.batch_size = batch_size;
177        self
178    }
179
180    /// Set shuffle
181    pub fn shuffle(mut self, shuffle: bool) -> Self {
182        self.shuffle = shuffle;
183        self
184    }
185
186    /// Set number of workers
187    pub fn num_workers(mut self, num_workers: usize) -> Self {
188        self.num_workers = num_workers;
189        self
190    }
191
192    /// Set pin memory
193    pub fn pin_memory(mut self, pin_memory: bool) -> Self {
194        self.pin_memory = pin_memory;
195        self
196    }
197
198    /// Set drop last
199    pub fn drop_last(mut self, drop_last: bool) -> Self {
200        self.drop_last = drop_last;
201        self
202    }
203
204    /// Set timeout
205    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
206        self.timeout = Some(timeout);
207        self
208    }
209
210    /// Set generator seed
211    pub fn generator(mut self, seed: u64) -> Self {
212        self.generator = Some(seed);
213        self
214    }
215
216    /// Set prefetch buffer size
217    pub fn prefetch_buffer_size(mut self, size: usize) -> Self {
218        self.prefetch_buffer_size = size;
219        self
220    }
221
222    /// Enable persistent workers
223    pub fn persistent_workers(mut self, persistent: bool) -> Self {
224        self.persistent_workers = persistent;
225        self
226    }
227
228    /// Set memory pinning configuration
229    pub fn pinning_config(mut self, config: PinningConfig) -> Self {
230        self.pinning_config = Some(config);
231        self
232    }
233
234    /// Create a configuration optimized for training
235    pub fn for_training() -> Self {
236        Self::new()
237            .batch_size(32)
238            .shuffle(true)
239            .num_workers(workers::utils::optimal_worker_count(false))
240            .pin_memory(true)
241            .prefetch_buffer_size(4)
242            .persistent_workers(true)
243    }
244
245    /// Create a configuration optimized for inference
246    pub fn for_inference() -> Self {
247        Self::new()
248            .batch_size(1)
249            .shuffle(false)
250            .num_workers(workers::utils::optimal_worker_count(true))
251            .pin_memory(false)
252            .prefetch_buffer_size(2)
253            .persistent_workers(false)
254    }
255
256    /// Create a configuration optimized for evaluation
257    pub fn for_evaluation() -> Self {
258        Self::new()
259            .batch_size(32)
260            .shuffle(false)
261            .num_workers(workers::utils::optimal_worker_count(false))
262            .pin_memory(false)
263            .prefetch_buffer_size(2)
264            .persistent_workers(false)
265            .drop_last(false)
266    }
267}
268
269/// Create a DataLoader with comprehensive configuration
270///
271/// Note: This function was removed due to lifetime issues with returning boxed iterators.
272/// Use DataLoader::builder() directly for configuration, or simple_dataloader()/simple_random_dataloader() for basic usage.
273///
274/// # Examples
275///
276/// ```rust,ignore
277/// use torsh_data::dataloader::{DataLoader, simple_dataloader, simple_random_dataloader};
278/// use torsh_data::dataset::TensorDataset;
279///
280/// let dataset = TensorDataset::new(vec![1, 2, 3, 4, 5]);
281///
282/// // Option 1: Use simple functions
283/// let dataloader = simple_dataloader(dataset, 2, false)?;
284/// for batch in dataloader.iter() {
285///     // Process batch
286/// }
287///
288/// // Option 2: Use builder pattern
289/// let dataloader = DataLoader::builder(dataset)
290///     .batch_size(16)
291///     .shuffle(true)
292///     .build()?;
293/// ```
294
295/// Utility functions for DataLoader operations
296pub mod utils {
297    use super::*;
298    use torsh_core::device::DeviceType;
299
300    /// Determine optimal DataLoader configuration for a given scenario
301    ///
302    /// # Arguments
303    ///
304    /// * `dataset_size` - Size of the dataset
305    /// * `scenario` - Training, inference, or evaluation scenario
306    /// * `target_device` - Target device for data
307    ///
308    /// # Returns
309    ///
310    /// Optimized DataLoaderConfig for the scenario
311    pub fn optimal_config(
312        dataset_size: usize,
313        scenario: &str,
314        target_device: Option<DeviceType>,
315    ) -> DataLoaderConfig {
316        let base_config = match scenario.to_lowercase().as_str() {
317            "training" | "train" => DataLoaderConfig::for_training(),
318            "inference" | "infer" => DataLoaderConfig::for_inference(),
319            "evaluation" | "eval" | "test" => DataLoaderConfig::for_evaluation(),
320            _ => DataLoaderConfig::new(),
321        };
322
323        let mut config = base_config;
324
325        // Adjust batch size based on dataset size
326        if dataset_size < 100 {
327            config = config.batch_size(dataset_size.min(8));
328        } else if dataset_size < 1000 {
329            config = config.batch_size(16);
330        } else {
331            config = config.batch_size(32);
332        }
333
334        // Configure memory pinning based on target device
335        if let Some(device) = target_device {
336            match device {
337                DeviceType::Cuda(device_id) => {
338                    config = config
339                        .pin_memory(true)
340                        .pinning_config(PinningConfig::cuda(device_id));
341                }
342                _ => {
343                    config = config.pin_memory(false);
344                }
345            }
346        }
347
348        config
349    }
350
351    /// Create a DataLoader with automatic configuration
352    ///
353    /// # Arguments
354    ///
355    /// * `dataset` - The dataset to load from
356    /// * `scenario` - Training scenario ("training", "inference", "evaluation")
357    ///
358    /// # Returns
359    ///
360    /// A DataLoader with automatically optimized configuration
361    ///
362    /// Note: This function was removed due to lifetime issues.
363    /// Use DataLoader::builder() with manual configuration instead.
364    // pub fn auto_dataloader(...) -- removed due to lifetime issues
365
366    /// Validate DataLoader configuration for common issues
367    ///
368    /// # Arguments
369    ///
370    /// * `config` - Configuration to validate
371    /// * `dataset_size` - Size of the dataset
372    ///
373    /// # Returns
374    ///
375    /// Result with validation messages or errors
376    pub fn validate_config(config: &DataLoaderConfig, dataset_size: usize) -> Result<Vec<String>> {
377        let mut warnings = Vec::new();
378
379        // Check batch size
380        if config.batch_size > dataset_size {
381            warnings.push(format!(
382                "Batch size ({}) is larger than dataset size ({})",
383                config.batch_size, dataset_size
384            ));
385        }
386
387        // Check worker configuration
388        if config.num_workers
389            > std::thread::available_parallelism()
390                .map(|n| n.get())
391                .unwrap_or(1)
392                * 2
393        {
394            warnings.push(format!(
395                "Number of workers ({}) may be too high for system capabilities",
396                config.num_workers
397            ));
398        }
399
400        // Check prefetch buffer size
401        if config.prefetch_buffer_size > 0 && config.num_workers == 0 {
402            warnings.push(
403                "Prefetching enabled but no workers configured, may not improve performance"
404                    .to_string(),
405            );
406        }
407
408        // Check memory pinning
409        if config.pin_memory && config.pinning_config.is_none() {
410            warnings
411                .push("Memory pinning enabled but no pinning configuration provided".to_string());
412        }
413
414        Ok(warnings)
415    }
416
417    /// Get performance recommendations for DataLoader configuration
418    ///
419    /// # Arguments
420    ///
421    /// * `dataset_size` - Size of the dataset
422    /// * `batch_size` - Current batch size
423    /// * `scenario` - Use case scenario
424    ///
425    /// # Returns
426    ///
427    /// Vector of performance recommendations
428    pub fn performance_recommendations(
429        dataset_size: usize,
430        batch_size: usize,
431        scenario: &str,
432    ) -> Vec<String> {
433        let mut recommendations = Vec::new();
434
435        // Batch size recommendations
436        if scenario == "training" && batch_size < 16 {
437            recommendations.push(
438                "Consider increasing batch size for training (recommended: 16-32)".to_string(),
439            );
440        }
441
442        if batch_size > dataset_size / 10 {
443            recommendations.push(
444                "Large batch size relative to dataset may reduce training effectiveness"
445                    .to_string(),
446            );
447        }
448
449        // Worker recommendations
450        let optimal_workers = workers::utils::optimal_worker_count(scenario == "inference");
451        recommendations.push(format!(
452            "Consider using {} workers for optimal performance",
453            optimal_workers
454        ));
455
456        // Prefetching recommendations
457        if scenario == "training" {
458            recommendations.push(
459                "Enable prefetching (buffer size 2-4) for better training performance".to_string(),
460            );
461        }
462
463        // Memory pinning recommendations
464        recommendations.push("Enable memory pinning if transferring data to GPU".to_string());
465
466        recommendations
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473    use crate::dataset::TensorDataset;
474    use torsh_core::device::DeviceType;
475
476    #[test]
477    fn test_dataloader_config() {
478        let config = DataLoaderConfig::new()
479            .batch_size(16)
480            .shuffle(true)
481            .num_workers(4)
482            .prefetch_buffer_size(2);
483
484        assert_eq!(config.batch_size, 16);
485        assert!(config.shuffle);
486        assert_eq!(config.num_workers, 4);
487        assert_eq!(config.prefetch_buffer_size, 2);
488    }
489
490    #[test]
491    fn test_training_config() {
492        let config = DataLoaderConfig::for_training();
493        assert!(config.shuffle);
494        assert!(config.persistent_workers);
495        assert!(config.pin_memory);
496        assert!(config.prefetch_buffer_size > 0);
497    }
498
499    #[test]
500    fn test_inference_config() {
501        let config = DataLoaderConfig::for_inference();
502        assert!(!config.shuffle);
503        assert!(!config.persistent_workers);
504        assert!(!config.pin_memory);
505    }
506
507    #[test]
508    fn test_evaluation_config() {
509        let config = DataLoaderConfig::for_evaluation();
510        assert!(!config.shuffle);
511        assert!(!config.drop_last);
512        assert!(!config.persistent_workers);
513    }
514
515    // test_create_dataloader removed - function was removed due to lifetime issues
516    // Use DataLoader::builder() directly instead
517
518    #[test]
519    fn test_utils_optimal_config() {
520        let config = utils::optimal_config(1000, "training", Some(DeviceType::Cuda(0)));
521        assert!(config.shuffle);
522        assert!(config.pin_memory);
523        assert!(config.pinning_config.is_some());
524    }
525
526    // test_utils_auto_dataloader removed - function was removed due to lifetime issues
527
528    #[test]
529    fn test_utils_validate_config() {
530        let config = DataLoaderConfig::new().batch_size(100);
531        let warnings = utils::validate_config(&config, 50).expect("utils should succeed");
532        assert!(!warnings.is_empty());
533        assert!(warnings[0].contains("Batch size"));
534    }
535
536    #[test]
537    fn test_utils_performance_recommendations() {
538        let recommendations = utils::performance_recommendations(1000, 8, "training");
539        assert!(!recommendations.is_empty());
540        assert!(recommendations.iter().any(|r| r.contains("batch size")));
541    }
542
543    #[test]
544    fn test_backward_compatibility() {
545        // Test that original API still works
546        use torsh_core::device::DeviceType;
547        use torsh_tensor::Tensor;
548
549        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0, 5.0], vec![5], DeviceType::Cpu)
550            .expect("Tensor should succeed");
551        let dataset = TensorDataset::from_tensor(tensor);
552        let dataloader =
553            simple_dataloader(dataset, 2, false).expect("simple dataloader should succeed");
554        assert_eq!(dataloader.len(), 3);
555    }
556
557    #[test]
558    fn test_prefetch_integration() {
559        use torsh_core::device::DeviceType;
560        use torsh_tensor::Tensor;
561
562        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0, 5.0], vec![5], DeviceType::Cpu)
563            .expect("Tensor should succeed");
564        let dataset = TensorDataset::from_tensor(tensor);
565        let dataloader = DataLoader::builder(dataset)
566            .batch_size(2)
567            .build()
568            .expect("build should succeed with valid configuration");
569
570        // Test regular iteration instead of prefetch to avoid lifetime issues
571        let mut iter = dataloader.iter();
572        let first_batch = iter
573            .next()
574            .expect("iterator should have a next element")
575            .expect("operation should succeed");
576        assert_eq!(first_batch.len(), 1); // Should have 1 stacked tensor
577
578        // Verify the tensor shape: [batch_size, sample_features]
579        // Original tensor is [5], each sample is [1], batched becomes [2, 1]
580        assert_eq!(first_batch[0].shape().dims(), &[2, 1]);
581    }
582}