Skip to main content

burn_core/data/dataloader/
builder.rs

1use super::{
2    BatchDataLoader, BatchStrategy, DataLoader, FixBatchStrategy, MultiThreadDataLoader,
3    batcher::Batcher,
4};
5use burn_dataset::Dataset;
6use burn_tensor::Device;
7use rand::{SeedableRng, rngs::StdRng};
8use std::sync::Arc;
9
10/// A builder for data loaders.
11pub struct DataLoaderBuilder<I, O> {
12    strategy: Option<Box<dyn BatchStrategy<I>>>,
13    batcher: Arc<dyn Batcher<I, O>>,
14    num_threads: Option<usize>,
15    shuffle: Option<u64>,
16    device: Option<Device>,
17}
18
19impl<I, O> DataLoaderBuilder<I, O>
20where
21    I: Send + Sync + Clone + std::fmt::Debug + 'static,
22    O: Send + Clone + std::fmt::Debug + 'static,
23{
24    /// Creates a new data loader builder.
25    ///
26    /// # Arguments
27    ///
28    /// * `batcher` - The batcher.
29    ///
30    /// # Returns
31    ///
32    /// The data loader builder.
33    pub fn new<Bt>(batcher: Bt) -> Self
34    where
35        Bt: Batcher<I, O> + 'static,
36    {
37        Self {
38            batcher: Arc::new(batcher),
39            strategy: None,
40            num_threads: None,
41            shuffle: None,
42            device: None,
43        }
44    }
45
46    /// Sets the batch size to a fix number.
47    ///
48    /// The [fix batch strategy](FixBatchStrategy) will be used.
49    ///
50    /// # Arguments
51    ///
52    /// * `batch_size` - The batch size.
53    ///
54    /// # Returns
55    ///
56    /// The data loader builder.
57    pub fn batch_size(mut self, batch_size: usize) -> Self {
58        self.strategy = Some(Box::new(FixBatchStrategy::new(batch_size)));
59        self
60    }
61
62    /// Sets the seed for shuffling.
63    ///
64    /// Each time the dataloader starts a new iteration, the dataset will be shuffled.
65    ///
66    /// # Arguments
67    ///
68    /// * `seed` - The seed.
69    ///
70    /// # Returns
71    ///
72    /// The data loader builder.
73    pub fn shuffle(mut self, seed: u64) -> Self {
74        self.shuffle = Some(seed);
75        self
76    }
77
78    /// Sets the number of workers.
79    ///
80    /// - `Some(0)` or `None`: the dataloader will run without work threads.
81    /// - `Some(n); n > 0`: the dataloader will run with `n` background threads.
82    ///
83    /// A 1-worker threaded dataloader will run loads in a background thread,
84    /// while a 0-worker threaded dataloader will run loads in the main thread.
85    ///
86    /// # Arguments
87    ///
88    /// * `num_workers` - The number of workers.
89    ///
90    /// # Returns
91    ///
92    /// The data loader builder.
93    pub fn num_workers(mut self, num_workers: usize) -> Self {
94        self.num_threads = Some(num_workers);
95        self
96    }
97
98    /// Sets the data loader device.
99    ///
100    /// # Arguments
101    ///
102    /// * `device` - The device to use when loading a batch.
103    ///
104    /// # Returns
105    ///
106    /// The data loader builder.
107    pub fn set_device(mut self, device: Device) -> Self {
108        self.device = Some(device);
109        self
110    }
111
112    /// Builds the data loader.
113    ///
114    /// # Arguments
115    ///
116    /// * `dataset` - The dataset.
117    ///
118    /// # Returns
119    ///
120    /// The data loader.
121    pub fn build<D>(self, dataset: D) -> Arc<dyn DataLoader<O>>
122    where
123        D: Dataset<I> + 'static,
124    {
125        let dataset = Arc::new(dataset);
126
127        let device = self.device.unwrap_or_default();
128        let rng = self.shuffle.map(StdRng::seed_from_u64);
129        let strategy = match self.strategy {
130            Some(strategy) => strategy,
131            None => Box::new(FixBatchStrategy::new(1)),
132        };
133
134        if let Some(num_threads) = self.num_threads
135            && num_threads > 0
136        {
137            return Arc::new(MultiThreadDataLoader::new(
138                strategy,
139                dataset,
140                self.batcher,
141                num_threads,
142                device,
143                rng,
144            ));
145        }
146
147        Arc::new(BatchDataLoader::new(
148            strategy,
149            dataset,
150            self.batcher,
151            device,
152            rng,
153        ))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    #[cfg(test)]
160    use burn_tensor::Device;
161
162    use super::*;
163    use crate::data::dataset::FakeDataset;
164
165    #[derive(new, Clone)]
166    struct TestBatcherDevice;
167
168    #[cfg(test)]
169    impl<I> Batcher<I, Device> for TestBatcherDevice {
170        fn batch(&self, _items: Vec<I>, device: &Device) -> Device {
171            device.clone()
172        }
173    }
174
175    #[test]
176    fn test_dataloader_no_workers() {
177        let default_device = Device::default();
178        let dataloader = DataLoaderBuilder::new(TestBatcherDevice::new())
179            .batch_size(1)
180            .build(FakeDataset::<String>::new(9));
181
182        assert_eq!(dataloader.num_items(), 9);
183
184        for device in dataloader.iter().map(Result::unwrap) {
185            assert_eq!(device, default_device)
186        }
187    }
188
189    #[test]
190    fn test_dataloader_default_device() {
191        let default_device = Device::default();
192        let dataloader = DataLoaderBuilder::new(TestBatcherDevice::new())
193            .batch_size(1)
194            .num_workers(1)
195            .build(FakeDataset::<String>::new(9));
196
197        assert_eq!(dataloader.num_items(), 9);
198
199        for device in dataloader.iter().map(Result::unwrap) {
200            assert_eq!(device, default_device)
201        }
202    }
203
204    #[test]
205    fn test_dataloader_slice_multi_device() {
206        let dataloader = DataLoaderBuilder::new(TestBatcherDevice::new())
207            .batch_size(1)
208            .num_workers(1)
209            .build(FakeDataset::<String>::new(11));
210
211        #[cfg(all(test, not(feature = "tch"), not(feature = "cuda")))]
212        // Only one device exists...
213        let (device1, device2) = (Device::flex(), Device::flex());
214
215        #[cfg(all(test, feature = "tch"))]
216        let (device1, device2) = (Device::libtorch_cuda(0), Device::libtorch_cuda(1));
217
218        #[cfg(all(test, feature = "cuda"))]
219        let (device1, device2) = (Device::cuda(0), Device::cuda(1));
220
221        assert_eq!(dataloader.num_items(), 11);
222        let dataloader_1 = dataloader.slice(0, 5).to_device(&device1);
223        let dataloader_2 = dataloader.slice(5, 11).to_device(&device2);
224
225        assert_eq!(dataloader_1.num_items(), 5);
226        assert_eq!(dataloader_2.num_items(), 6);
227
228        let (mut iterator_1, mut iterator_2) = (dataloader_1.iter(), dataloader_2.iter());
229
230        for _ in 0..5 {
231            assert_eq!(iterator_1.next().unwrap().unwrap(), device1);
232            assert_eq!(iterator_2.next().unwrap().unwrap(), device2);
233        }
234
235        assert!(iterator_1.next().is_none());
236        // For uneven split, the last dataloader (partial dataset) will have the remaining item
237        assert_eq!(iterator_2.next().unwrap().unwrap(), device2);
238        assert!(iterator_2.next().is_none());
239    }
240}