Skip to main content

dataloader_rs/loader/
dataloader.rs

1use std::sync::Arc;
2
3use crate::{
4    collator::{Collator, VecCollator},
5    dataset::Dataset,
6    error::Result,
7    loader::{
8        builder::DataLoaderBuilder,
9        iter::{DataLoaderIter, IndexedIter},
10        pool::LoaderPool,
11        source::{EpochSettings, LoaderSource, SyncSource},
12    },
13    sampler::{BatchSampler, Sampler, SequentialSampler},
14    transform::{Identity, Transform},
15};
16#[cfg(feature = "async")]
17use crate::{dataset::AsyncDataset, loader::source::AsyncSource};
18
19/// High-performance DataLoader with a PyTorch-like interface.
20///
21/// The dataset fetches items; the loader applies its [`Transform`] to each one
22/// (`T`, [`Identity`] unless set with
23/// [`transform`](DataLoaderBuilder::transform)), then collates them with `C`.
24/// `Src` holds the dataset: [`SyncSource`] for a [`Dataset`], `AsyncSource` for
25/// an `AsyncDataset` (feature `async`).
26pub struct DataLoader<Src, S: Sampler, C, T = Identity> {
27    pub(super) source: Src,
28    pub(super) batch_sampler: BatchSampler<S>,
29    pub(super) transform: Arc<T>,
30    pub(super) collator: Arc<C>,
31    pub(super) prefetch_depth: usize,
32    /// Batches processed in parallel. `0` means the direct path: batches are
33    /// processed in `Iterator::next`.
34    pub(super) num_workers: usize,
35    /// Items of one batch fetched in parallel; `1` uses `Dataset::get_batch`.
36    pub(super) item_concurrency: usize,
37    /// The rayon pool running batch tasks and item lanes, with
38    /// `max(num_workers, 1) * item_concurrency` threads. `None` when both
39    /// settings are at their defaults.
40    pub(super) pool: Option<LoaderPool>,
41}
42
43impl<D: Dataset> DataLoader<SyncSource<D>, SequentialSampler, VecCollator> {
44    /// Create a [`DataLoaderBuilder`] starting with [`SequentialSampler`] and
45    /// [`VecCollator`] as defaults.
46    pub fn builder(dataset: D) -> DataLoaderBuilder<SyncSource<D>, SequentialSampler, VecCollator> {
47        DataLoaderBuilder::new(SyncSource::new(dataset))
48    }
49}
50
51#[cfg(feature = "async")]
52impl<A: AsyncDataset> DataLoader<AsyncSource<A>, SequentialSampler, VecCollator> {
53    /// Create a [`DataLoaderBuilder`] for an [`AsyncDataset`], with the same options and
54    /// defaults as [`builder`](DataLoader::builder).
55    ///
56    /// Items are fetched by futures on a tokio runtime (the loader's own, one thread per CPU
57    /// by default, see [`runtime_threads`](DataLoaderBuilder::runtime_threads); or the
58    /// caller's through [`runtime`](DataLoaderBuilder::runtime)) and collated on the loader's
59    /// thread pool, then returned in epoch order by the same iterator. `num_workers` batches
60    /// (at least one) are fetched at once, each with `item_concurrency` items in flight: at
61    /// most `num_workers × item_concurrency` fetches run at a time.
62    pub fn builder_async(
63        dataset: A,
64    ) -> DataLoaderBuilder<AsyncSource<A>, SequentialSampler, VecCollator> {
65        DataLoaderBuilder::new(AsyncSource::new(dataset))
66    }
67}
68
69impl<Src, S, C, T> DataLoader<Src, S, C, T>
70where
71    Src: LoaderSource,
72    S: Sampler,
73    T: Transform<Src::Item>,
74    C: Collator<T::Output>,
75{
76    /// Start one epoch of iteration.
77    ///
78    /// Advances the sampler (hence `&mut self`); the returned iterator does
79    /// not borrow the loader.
80    pub fn iter(&mut self) -> DataLoaderIter<Src, C, T> {
81        let chunks = self.batch_sampler.batch_indices(self.source.len());
82        self.iter_chunks(chunks)
83    }
84
85    /// Start one epoch, like [`iter`](Self::iter), whose batches come with
86    /// the dataset indices of their items: `(indices, batch)`, `indices[k]`
87    /// being the index of the batch's `k`-th item as the sampler drew it. A
88    /// failed batch is still one step of the epoch: the next batch gets the
89    /// next indices.
90    ///
91    /// ```rust
92    /// use dataloader_rs::{DataLoader, Dataset, RandomSampler, error::Result};
93    ///
94    /// struct Squares;
95    ///
96    /// impl Dataset for Squares {
97    ///     type Item = usize;
98    ///     fn get(&self, index: usize) -> Result<usize> { Ok(index * index) }
99    ///     fn len(&self) -> usize { 10 }
100    /// }
101    ///
102    /// let mut loader = DataLoader::builder(Squares)
103    ///     .batch_size(4)
104    ///     .sampler(RandomSampler::new(7))
105    ///     .build();
106    /// for batch in loader.iter_with_indices() {
107    ///     let (indices, squares) = batch.unwrap();
108    ///     assert!(indices.iter().zip(squares).all(|(i, s)| i * i == s));
109    /// }
110    /// ```
111    pub fn iter_with_indices(&mut self) -> IndexedIter<Src, C, T> {
112        let chunks = self.batch_sampler.batch_indices(self.source.len());
113        IndexedIter::new(chunks.clone(), self.iter_chunks(chunks))
114    }
115
116    /// Start one epoch over `chunks`, the batches of indices of
117    /// [`epoch_chunks`](Self::epoch_chunks).
118    pub(crate) fn iter_chunks(&mut self, chunks: Vec<Vec<usize>>) -> DataLoaderIter<Src, C, T> {
119        let settings = EpochSettings {
120            num_workers: self.num_workers,
121            item_concurrency: self.item_concurrency,
122            prefetch_depth: self.prefetch_depth,
123        };
124        let pool = self.pool.as_mut().map(LoaderPool::get);
125        DataLoaderIter::new(self.source.epoch(
126            &self.transform,
127            &self.collator,
128            pool,
129            settings,
130            chunks,
131        ))
132    }
133
134    /// Reference to the underlying dataset.
135    pub fn dataset(&self) -> &Src::Dataset {
136        self.source.dataset()
137    }
138
139    /// Reference to the transform.
140    pub fn transform(&self) -> &T {
141        &self.transform
142    }
143
144    /// Reference to the collator.
145    pub fn collator(&self) -> &C {
146        &self.collator
147    }
148
149    /// Whether batches run on the loader's pool (`num_workers > 0` or
150    /// `item_concurrency > 1`) rather than in `next` on the caller's thread.
151    #[cfg(feature = "python")]
152    pub(crate) fn has_pool(&self) -> bool {
153        self.pool.is_some()
154    }
155
156    /// The loader's pool, if it has one, without keeping it alive.
157    #[cfg(feature = "python")]
158    pub(crate) fn pool_handle(&self) -> Option<std::sync::Weak<rayon::ThreadPool>> {
159        self.pool.as_ref().map(LoaderPool::handle)
160    }
161
162    #[cfg(feature = "python")]
163    pub(crate) fn sampler(&self) -> &S {
164        &self.batch_sampler.sampler
165    }
166
167    #[cfg(feature = "python")]
168    pub(crate) fn sampler_mut(&mut self) -> &mut S {
169        &mut self.batch_sampler.sampler
170    }
171
172    /// Generate batch index chunks for one epoch, advancing the sampler.
173    #[cfg(feature = "python")]
174    pub(crate) fn epoch_chunks(&mut self) -> Vec<Vec<usize>> {
175        self.batch_sampler.batch_indices(self.source.len())
176    }
177
178    /// Number of batches `n_indices` sampled indices are grouped into, for a
179    /// sampler whose length is measured outside the core.
180    #[cfg(feature = "python")]
181    pub(crate) fn num_batches(&self, n_indices: usize) -> usize {
182        self.batch_sampler.num_batches(n_indices)
183    }
184
185    /// Total number of samples in the dataset.
186    pub fn len(&self) -> usize {
187        self.source.len()
188    }
189
190    /// Total number of batches for one epoch with current batch settings.
191    ///
192    /// Counts what the sampler yields, so a sharding or subsampling sampler
193    /// (e.g. [`DistributedSampler`](crate::DistributedSampler)) is accounted for.
194    pub fn batch_len(&self) -> usize {
195        self.batch_sampler.len(self.source.len())
196    }
197
198    /// Returns `true` when the dataset is empty.
199    pub fn is_empty(&self) -> bool {
200        self.source.len() == 0
201    }
202}
203
204impl<Src, S, C, T> IntoIterator for &mut DataLoader<Src, S, C, T>
205where
206    Src: LoaderSource,
207    S: Sampler,
208    T: Transform<Src::Item>,
209    C: Collator<T::Output>,
210{
211    type Item = Result<C::Batch>;
212    type IntoIter = DataLoaderIter<Src, C, T>;
213
214    fn into_iter(self) -> Self::IntoIter {
215        self.iter()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use std::{
222        collections::HashSet,
223        sync::Arc as StdArc,
224        sync::atomic::{AtomicUsize, Ordering},
225        thread,
226        time::Duration,
227    };
228
229    use crate::{
230        collator::{Collator, VecCollator},
231        dataset::Dataset,
232        error::Result,
233        loader::{DataLoader, SyncSource, pool::LoaderPool},
234        sampler::{RandomSampler, Sampler, SequentialSampler},
235    };
236
237    /// Returns `index` as its item. Never fails.
238    struct CountingDs {
239        len: usize,
240    }
241
242    impl Dataset for CountingDs {
243        type Item = usize;
244
245        fn get(&self, index: usize) -> Result<usize> {
246            Ok(index)
247        }
248
249        fn len(&self) -> usize {
250            self.len
251        }
252    }
253
254    /// Tracks concurrent calls to `get` so tests can assert parallel execution.
255    struct ConcurrentDs {
256        len: usize,
257        peak: StdArc<AtomicUsize>,
258        active: StdArc<AtomicUsize>,
259    }
260
261    impl Dataset for ConcurrentDs {
262        type Item = usize;
263
264        fn get(&self, index: usize) -> Result<usize> {
265            let current = self.active.fetch_add(1, Ordering::SeqCst) + 1;
266            self.peak.fetch_max(current, Ordering::SeqCst);
267            thread::sleep(Duration::from_micros(100));
268            self.active.fetch_sub(1, Ordering::SeqCst);
269            Ok(index)
270        }
271
272        fn len(&self) -> usize {
273            self.len
274        }
275    }
276
277    /// Always returns an error.
278    struct ErrDs;
279
280    impl Dataset for ErrDs {
281        type Item = u8;
282
283        fn get(&self, _: usize) -> Result<u8> {
284            Err("dataset failure".into())
285        }
286
287        fn len(&self) -> usize {
288            4
289        }
290    }
291
292    struct PartialErrDs {
293        fail_at: usize,
294        len: usize,
295    }
296
297    impl Dataset for PartialErrDs {
298        type Item = usize;
299
300        fn get(&self, index: usize) -> Result<Self::Item> {
301            if index == self.fail_at {
302                Err(format!("fail at index {index}").into())
303            } else {
304                Ok(index)
305            }
306        }
307
308        fn len(&self) -> usize {
309            self.len
310        }
311    }
312
313    fn counting(len: usize) -> DataLoader<SyncSource<CountingDs>, SequentialSampler, VecCollator> {
314        DataLoader::builder(CountingDs { len }).build()
315    }
316
317    #[test]
318    fn builder_defaults() {
319        let mut loader = counting(5);
320        assert_eq!(loader.iter().count(), 5);
321    }
322
323    #[test]
324    fn builder_batch_size() {
325        let mut loader = DataLoader::builder(CountingDs { len: 10 })
326            .batch_size(3)
327            .build();
328        let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
329        assert_eq!(batches.len(), 4);
330        assert_eq!(batches[0].len(), 3);
331        assert_eq!(batches[3].len(), 1);
332    }
333
334    #[test]
335    fn builder_drop_last() {
336        let mut loader = DataLoader::builder(CountingDs { len: 10 })
337            .batch_size(3)
338            .drop_last(true)
339            .build();
340        let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
341        assert_eq!(batches.len(), 3);
342        for b in &batches {
343            assert_eq!(b.len(), 3);
344        }
345    }
346
347    #[test]
348    fn builder_no_drop_last_exact_divisor() {
349        let mut loader = DataLoader::builder(CountingDs { len: 9 })
350            .batch_size(3)
351            .drop_last(true)
352            .build();
353        assert_eq!(loader.iter().count(), 3);
354    }
355
356    #[test]
357    #[should_panic]
358    fn builder_zero_batch_size_panics() {
359        DataLoader::builder(CountingDs { len: 5 })
360            .batch_size(0)
361            .build();
362    }
363
364    #[test]
365    #[should_panic]
366    fn builder_zero_prefetch_depth_panics() {
367        DataLoader::builder(CountingDs { len: 5 })
368            .prefetch_depth(0)
369            .build();
370    }
371
372    #[test]
373    fn sequential_batch_values() {
374        let mut loader = DataLoader::builder(CountingDs { len: 6 })
375            .batch_size(2)
376            .build();
377        let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
378        assert_eq!(batches, vec![vec![0, 1], vec![2, 3], vec![4, 5]]);
379    }
380
381    #[test]
382    fn sequential_single_item_batches() {
383        let mut loader = counting(4);
384        let items: Vec<usize> = loader
385            .iter()
386            .map(|b| b.unwrap().into_iter().next().unwrap())
387            .collect();
388        assert_eq!(items, vec![0, 1, 2, 3]);
389    }
390
391    #[test]
392    fn empty_dataset_yields_no_batches() {
393        let mut loader = counting(0);
394        assert_eq!(loader.iter().count(), 0);
395    }
396
397    #[test]
398    fn single_item_dataset() {
399        let mut loader = counting(1);
400        let batches: Vec<_> = loader.iter().collect();
401        assert_eq!(batches.len(), 1);
402        assert_eq!(batches[0].as_ref().unwrap(), &[0]);
403    }
404
405    #[test]
406    fn all_indices_covered_sequential() {
407        let n = 25;
408        let mut loader = DataLoader::builder(CountingDs { len: n })
409            .batch_size(4)
410            .build();
411        let seen: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
412        assert_eq!(seen.len(), n);
413    }
414
415    #[test]
416    fn all_indices_covered_drop_last() {
417        let n = 7;
418        let bs = 3;
419        let mut loader = DataLoader::builder(CountingDs { len: n })
420            .batch_size(bs)
421            .drop_last(true)
422            .build();
423        let seen: Vec<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
424        assert_eq!(seen.len(), 6);
425    }
426
427    #[test]
428    fn exact_size_no_drop_last() {
429        let mut loader = DataLoader::builder(CountingDs { len: 10 })
430            .batch_size(3)
431            .build();
432        let iter = loader.iter();
433        assert_eq!(iter.len(), 4);
434    }
435
436    #[test]
437    fn exact_size_with_drop_last() {
438        let mut loader = DataLoader::builder(CountingDs { len: 10 })
439            .batch_size(3)
440            .drop_last(true)
441            .build();
442        let iter = loader.iter();
443        assert_eq!(iter.len(), 3);
444    }
445
446    #[test]
447    fn exact_size_decrements_as_consumed() {
448        let mut loader = DataLoader::builder(CountingDs { len: 6 })
449            .batch_size(2)
450            .build();
451        let mut iter = loader.iter();
452        assert_eq!(iter.len(), 3);
453        iter.next();
454        assert_eq!(iter.len(), 2);
455        iter.next();
456        assert_eq!(iter.len(), 1);
457        iter.next();
458        assert_eq!(iter.len(), 0);
459        assert!(iter.next().is_none());
460    }
461
462    #[test]
463    fn loader_is_reusable_across_epochs() {
464        let mut loader = DataLoader::builder(CountingDs { len: 6 })
465            .batch_size(2)
466            .build();
467        let epoch1: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
468        let epoch2: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
469        assert_eq!(epoch1, epoch2);
470    }
471
472    #[test]
473    fn random_sampler_differs_across_epochs() {
474        let mut loader = DataLoader::builder(CountingDs { len: 20 })
475            .batch_size(4)
476            .sampler(RandomSampler::new(0))
477            .build();
478        let e1: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
479        let e2: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
480        assert_ne!(e1, e2);
481    }
482
483    #[test]
484    fn all_indices_covered_random() {
485        let n = 50;
486        let mut loader = DataLoader::builder(CountingDs { len: n })
487            .batch_size(5)
488            .sampler(RandomSampler::new(1))
489            .build();
490        let seen: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
491        assert_eq!(seen.len(), n);
492    }
493
494    #[test]
495    fn parallel_fetch_produces_same_items() {
496        let n = 30;
497        let bs = 5;
498
499        let mut seq = DataLoader::builder(CountingDs { len: n })
500            .batch_size(bs)
501            .build();
502        let mut par = DataLoader::builder(CountingDs { len: n })
503            .batch_size(bs)
504            .num_workers(4)
505            .build();
506
507        let seq_out: Vec<Vec<usize>> = seq.iter().map(|b| b.unwrap()).collect();
508        let par_out: Vec<Vec<usize>> = par.iter().map(|b| b.unwrap()).collect();
509        assert_eq!(seq_out, par_out);
510    }
511
512    #[test]
513    fn parallel_fetch_actually_concurrent() {
514        let peak = StdArc::new(AtomicUsize::new(0));
515        let active = StdArc::new(AtomicUsize::new(0));
516
517        let ds = ConcurrentDs {
518            len: 16,
519            peak: StdArc::clone(&peak),
520            active: StdArc::clone(&active),
521        };
522
523        let mut loader = DataLoader::builder(ds)
524            .batch_size(8)
525            .num_workers(4)
526            .prefetch_depth(2)
527            .build();
528
529        loader.iter().for_each(|b| {
530            b.unwrap();
531        });
532
533        assert!(
534            peak.load(Ordering::SeqCst) > 1,
535            "expected concurrent get() calls, but peak concurrency was 1"
536        );
537    }
538
539    #[test]
540    fn dataset_error_propagates() {
541        let mut loader = DataLoader::builder(ErrDs).batch_size(2).build();
542        let first = loader.iter().next().unwrap();
543        assert!(first.is_err());
544    }
545
546    #[test]
547    fn dataset_error_does_not_panic_loader() {
548        let mut loader = DataLoader::builder(ErrDs).batch_size(2).build();
549        for batch in loader.iter() {
550            let _ = batch;
551        }
552        let count = loader.iter().count();
553        assert_eq!(count, 2);
554    }
555
556    #[test]
557    fn iter_catches_error_from_dataset_get() {
558        let mut loader = DataLoader::builder(PartialErrDs { fail_at: 3, len: 8 })
559            .batch_size(4)
560            .build();
561
562        let first = loader.iter().next().expect("missing first batch");
563        assert!(
564            first.is_err(),
565            "first batch should surface dataset get() error"
566        );
567    }
568
569    #[test]
570    fn collate_is_skipped_when_get_fails_and_error_is_iterated() {
571        struct CountingCollator {
572            calls: StdArc<AtomicUsize>,
573        }
574
575        impl Collator<usize> for CountingCollator {
576            type Batch = Vec<usize>;
577
578            fn collate(&self, items: Vec<usize>) -> Result<Self::Batch> {
579                self.calls.fetch_add(1, Ordering::SeqCst);
580                Ok(items)
581            }
582        }
583
584        let calls = StdArc::new(AtomicUsize::new(0));
585        let collator = CountingCollator {
586            calls: StdArc::clone(&calls),
587        };
588
589        let mut loader = DataLoader::builder(PartialErrDs { fail_at: 3, len: 8 })
590            .batch_size(4)
591            .collator(collator)
592            .build();
593
594        let results: Vec<_> = loader.iter().collect();
595        assert_eq!(results.len(), 2);
596        assert!(results[0].is_err(), "first batch should be an error");
597        assert!(results[1].is_ok(), "second batch should succeed");
598        assert_eq!(
599            calls.load(Ordering::SeqCst),
600            1,
601            "collate should be called only for successful batches"
602        );
603    }
604
605    #[test]
606    fn custom_collator_sum() {
607        struct SumCollator;
608        impl Collator<usize> for SumCollator {
609            type Batch = usize;
610            fn collate(&self, items: Vec<usize>) -> Result<usize> {
611                Ok(items.iter().sum())
612            }
613        }
614
615        let mut loader = DataLoader::builder(CountingDs { len: 6 })
616            .batch_size(3)
617            .collator(SumCollator)
618            .build();
619
620        let sums: Vec<usize> = loader.iter().map(|b| b.unwrap()).collect();
621        assert_eq!(sums, vec![3, 12]);
622    }
623
624    #[test]
625    fn item_concurrency_produces_correct_results() {
626        let n = 20;
627        let mut loader = DataLoader::builder(CountingDs { len: n })
628            .batch_size(4)
629            .item_concurrency(2)
630            .build();
631        let seen: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
632        assert_eq!(seen.len(), n);
633    }
634
635    #[test]
636    fn item_concurrency_with_num_workers() {
637        let n = 24;
638        let mut loader = DataLoader::builder(CountingDs { len: n })
639            .batch_size(4)
640            .num_workers(2)
641            .item_concurrency(2)
642            .build();
643        let seen: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
644        assert_eq!(seen.len(), n);
645    }
646
647    #[test]
648    fn item_concurrency_fetches_a_single_item_with_get() {
649        struct GetOnly;
650        impl Dataset for GetOnly {
651            type Item = usize;
652            fn get(&self, index: usize) -> Result<usize> {
653                Ok(index)
654            }
655            fn get_batch(&self, _: &[usize]) -> Result<Vec<usize>> {
656                Err("get_batch must not be used with item_concurrency > 1".into())
657            }
658            fn len(&self) -> usize {
659                7
660            }
661        }
662
663        let mut loader = DataLoader::builder(GetOnly)
664            .batch_size(3)
665            .item_concurrency(2)
666            .build();
667        let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
668        assert_eq!(batches, vec![vec![0, 1, 2], vec![3, 4, 5], vec![6]]);
669    }
670
671    #[test]
672    fn huge_prefetch_depth_neither_allocates_nor_wraps() {
673        for depth in [1 << 40, usize::MAX] {
674            let mut loader = DataLoader::builder(CountingDs { len: 10 })
675                .batch_size(3)
676                .num_workers(1)
677                .prefetch_depth(depth)
678                .build();
679            let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
680            assert_eq!(batches.len(), 4);
681        }
682    }
683
684    #[test]
685    fn early_drop_does_not_hang() {
686        let mut loader = DataLoader::builder(CountingDs { len: 100 })
687            .batch_size(5)
688            .prefetch_depth(2)
689            .num_workers(2)
690            .build();
691
692        let mut iter = loader.iter();
693        iter.next();
694        drop(iter);
695
696        let count = DataLoader::builder(CountingDs { len: 10 })
697            .batch_size(2)
698            .build()
699            .iter()
700            .count();
701        assert_eq!(count, 5);
702    }
703
704    #[test]
705    fn early_break_then_new_epoch() {
706        let mut loader = DataLoader::builder(CountingDs { len: 20 })
707            .batch_size(4)
708            .prefetch_depth(4)
709            .num_workers(2)
710            .build();
711
712        for (i, b) in (&mut loader).into_iter().enumerate() {
713            b.unwrap();
714            if i == 1 {
715                break;
716            }
717        }
718
719        let second: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
720        assert_eq!(second.len(), 5);
721
722        let all_items: HashSet<usize> = second.into_iter().flatten().collect();
723        assert_eq!(all_items.len(), 20);
724    }
725
726    struct PanicDs;
727
728    impl Dataset for PanicDs {
729        type Item = usize;
730
731        fn get(&self, index: usize) -> Result<usize> {
732            if index == 5 {
733                panic!("dataset panicked at {index}");
734            }
735            Ok(index)
736        }
737
738        fn len(&self) -> usize {
739            40
740        }
741    }
742
743    #[test]
744    #[should_panic(expected = "dataset panicked at 5")]
745    fn worker_panic_propagates_to_consumer() {
746        let mut loader = DataLoader::builder(PanicDs)
747            .batch_size(4)
748            .num_workers(2)
749            .build();
750        // Before the fix the epoch silently ended after the first batch.
751        for batch in loader.iter() {
752            let _ = batch;
753        }
754    }
755
756    #[test]
757    #[should_panic(expected = "dataset panicked at 5")]
758    fn item_concurrency_panic_propagates_to_consumer() {
759        let mut loader = DataLoader::builder(PanicDs)
760            .batch_size(4)
761            .num_workers(2)
762            .item_concurrency(2)
763            .build();
764        for batch in loader.iter() {
765            let _ = batch;
766        }
767    }
768
769    #[test]
770    fn loader_is_reusable_after_worker_panic() {
771        let mut loader = DataLoader::builder(PanicDs)
772            .batch_size(4)
773            .num_workers(2)
774            .build();
775        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
776            loader.iter().for_each(drop);
777        }));
778        assert!(caught.is_err());
779        // The next epoch still reaches (and re-raises) the same panic rather
780        // than hanging or truncating.
781        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
782            loader.iter().for_each(drop);
783        }));
784        assert!(caught.is_err());
785    }
786
787    /// Batch 0 sleeps; every other batch is instant.
788    struct SlowFirstDs(StdArc<AtomicUsize>);
789
790    impl Dataset for SlowFirstDs {
791        type Item = usize;
792
793        fn get(&self, index: usize) -> Result<usize> {
794            if index == 0 {
795                thread::sleep(Duration::from_millis(200));
796            }
797            self.0.fetch_add(1, Ordering::SeqCst);
798            Ok(index)
799        }
800
801        fn len(&self) -> usize {
802            1000
803        }
804    }
805
806    #[test]
807    fn slow_batch_does_not_buffer_whole_epoch() {
808        // While the consumer waits on batch 0, fast batches must not pile up
809        // in the reorder buffer beyond num_workers + prefetch_depth.
810        let (workers, depth) = (2, 2);
811        let fetched = StdArc::new(AtomicUsize::new(0));
812        let mut loader = DataLoader::builder(SlowFirstDs(StdArc::clone(&fetched)))
813            .num_workers(workers)
814            .prefetch_depth(depth)
815            .build();
816
817        let mut iter = loader.iter();
818        assert_eq!(iter.next().unwrap().unwrap(), vec![0]);
819        // Returning batch 0 may let one more batch start.
820        let fetched_now = fetched.load(Ordering::SeqCst);
821        assert!(
822            fetched_now <= workers + depth + 1,
823            "{fetched_now} batches fetched while waiting on batch 0"
824        );
825        assert_eq!(iter.count(), 999);
826    }
827
828    #[test]
829    fn dropping_iterator_does_not_wait_for_running_batch() {
830        let mut loader = DataLoader::builder(SlowFirstDs(StdArc::new(AtomicUsize::new(0))))
831            .num_workers(2)
832            .build();
833        let iter = loader.iter();
834        thread::sleep(Duration::from_millis(20)); // batch 0 is now sleeping
835        let start = std::time::Instant::now();
836        drop(iter);
837        assert!(start.elapsed() < Duration::from_millis(100));
838        // The loader is immediately reusable.
839        assert_eq!(loader.iter().nth(1).unwrap().unwrap(), vec![1]);
840    }
841
842    #[test]
843    fn batch_and_item_work_share_one_pool() {
844        struct ThreadNameDs(std::sync::Mutex<HashSet<String>>);
845        impl Dataset for ThreadNameDs {
846            type Item = usize;
847            fn get(&self, index: usize) -> Result<usize> {
848                let name = thread::current().name().unwrap_or("?").to_owned();
849                self.0.lock().unwrap().insert(name);
850                thread::sleep(Duration::from_micros(200));
851                Ok(index)
852            }
853            fn len(&self) -> usize {
854                64
855            }
856        }
857
858        let mut loader = DataLoader::builder(ThreadNameDs(Default::default()))
859            .batch_size(8)
860            .num_workers(2)
861            .item_concurrency(3)
862            .build();
863        loader.iter().for_each(|b| drop(b.unwrap()));
864
865        let names = loader.dataset().0.lock().unwrap().clone();
866        assert!(
867            names.iter().all(|n| n.starts_with("dataloader-")),
868            "items fetched outside the loader pool: {names:?}"
869        );
870        assert!(names.len() <= 6, "pool must have 2 x 3 threads: {names:?}");
871    }
872
873    fn pool_threads<S: Sampler, C>(
874        loader: &DataLoader<SyncSource<CountingDs>, S, C>,
875    ) -> Option<usize> {
876        loader.pool.as_ref().map(LoaderPool::threads)
877    }
878
879    #[test]
880    fn pool_size_is_workers_times_item_concurrency() {
881        let ds = || CountingDs { len: 8 };
882        assert_eq!(pool_threads(&DataLoader::builder(ds()).build()), None);
883        assert_eq!(
884            pool_threads(&DataLoader::builder(ds()).num_workers(3).build()),
885            Some(3)
886        );
887        assert_eq!(
888            pool_threads(&DataLoader::builder(ds()).item_concurrency(4).build()),
889            Some(4)
890        );
891        assert_eq!(
892            pool_threads(
893                &DataLoader::builder(ds())
894                    .num_workers(2)
895                    .item_concurrency(5)
896                    .build()
897            ),
898            Some(10)
899        );
900    }
901
902    /// Sleeps per item and records the most `get` calls running at once.
903    struct ConcurrencyDs {
904        running: AtomicUsize,
905        peak: AtomicUsize,
906    }
907
908    impl Dataset for ConcurrencyDs {
909        type Item = usize;
910        fn get(&self, index: usize) -> Result<usize> {
911            let now = self.running.fetch_add(1, Ordering::SeqCst) + 1;
912            self.peak.fetch_max(now, Ordering::SeqCst);
913            thread::sleep(Duration::from_millis(10));
914            self.running.fetch_sub(1, Ordering::SeqCst);
915            Ok(index)
916        }
917        fn len(&self) -> usize {
918            24
919        }
920    }
921
922    #[test]
923    fn item_concurrency_bounds_item_concurrency() {
924        let ds = || ConcurrencyDs {
925            running: AtomicUsize::new(0),
926            peak: AtomicUsize::new(0),
927        };
928        // One batch at a time (direct path): exactly 3 items in flight.
929        let mut loader = DataLoader::builder(ds())
930            .batch_size(12)
931            .item_concurrency(3)
932            .build();
933        let items: Vec<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
934        assert_eq!(items, (0..24).collect::<Vec<_>>());
935        assert_eq!(loader.dataset().peak.load(Ordering::SeqCst), 3);
936
937        // Two batches in parallel, 2 items each: at most 4 in flight.
938        let mut loader = DataLoader::builder(ds())
939            .batch_size(12)
940            .num_workers(2)
941            .item_concurrency(2)
942            .build();
943        let items: Vec<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
944        assert_eq!(items, (0..24).collect::<Vec<_>>());
945        let peak = loader.dataset().peak.load(Ordering::SeqCst);
946        assert!((2..=4).contains(&peak), "peak {peak}");
947    }
948
949    #[test]
950    fn leaked_iterator_keeps_dataset_alive() {
951        struct DropFlagDs(StdArc<AtomicUsize>);
952        impl Dataset for DropFlagDs {
953            type Item = usize;
954            fn get(&self, index: usize) -> Result<usize> {
955                Ok(index)
956            }
957            fn len(&self) -> usize {
958                64
959            }
960        }
961        impl Drop for DropFlagDs {
962            fn drop(&mut self) {
963                self.0.fetch_add(1, Ordering::SeqCst);
964            }
965        }
966
967        let drops = StdArc::new(AtomicUsize::new(0));
968        let mut loader = DataLoader::builder(DropFlagDs(StdArc::clone(&drops)))
969            .num_workers(2)
970            .build();
971        std::mem::forget(loader.iter());
972        drop(loader);
973        // Leaked workers still hold the dataset: it must not have been freed.
974        assert_eq!(drops.load(Ordering::SeqCst), 0);
975    }
976
977    #[test]
978    fn batch_len_follows_distributed_sampler() {
979        use crate::sampler::DistributedSampler;
980
981        let mut loader = DataLoader::builder(CountingDs { len: 10 })
982            .batch_size(2)
983            .sampler(DistributedSampler::new(SequentialSampler, 0, 4))
984            .build();
985        // 10 items over 4 ranks -> 3 per rank -> 2 batches of size 2.
986        assert_eq!(loader.batch_len(), 2);
987        assert_eq!(loader.iter().count(), 2);
988    }
989
990    #[test]
991    fn loader_len() {
992        let loader = counting(42);
993        assert_eq!(loader.len(), 42);
994        assert!(!loader.is_empty());
995    }
996
997    #[test]
998    fn loader_is_empty() {
999        let loader = counting(0);
1000        assert!(loader.is_empty());
1001        assert_eq!(loader.len(), 0);
1002    }
1003
1004    #[test]
1005    fn dataset_accessor() {
1006        let loader = counting(7);
1007        assert_eq!(loader.dataset().len(), 7);
1008    }
1009
1010    #[cfg(feature = "torch-rs")]
1011    #[test]
1012    fn pin_memory_true_keeps_tensor_batches_valid() {
1013        use crate::collator::DefaultCollator;
1014        use tch::{Device, Kind, Tensor};
1015
1016        struct TensorDs(usize);
1017        impl Dataset for TensorDs {
1018            type Item = Tensor;
1019
1020            fn get(&self, _index: usize) -> Result<Self::Item> {
1021                Tensor::f_zeros([2, 3], (Kind::Float, Device::Cpu)).map_err(|e| e.into())
1022            }
1023
1024            fn len(&self) -> usize {
1025                self.0
1026            }
1027        }
1028
1029        let mut loader = DataLoader::builder(TensorDs(4))
1030            .batch_size(2)
1031            .collator(DefaultCollator)
1032            .pin_memory(true)
1033            .build();
1034
1035        for batch in loader.iter() {
1036            let batch = batch.unwrap();
1037            assert_eq!(batch.size(), vec![2, 2, 3]);
1038        }
1039    }
1040}