Skip to main content

ruda_model/data/dataloader/
batch.rs

1use super::{BatchStrategy, DataLoader, DataLoaderIterator, Progress, batcher::Batcher};
2use ruda_dataset::{
3    Dataset,
4    transform::{PartialDataset, ShuffledDataset},
5};
6use ruda_tensor::api::backend::Backend;
7use rand::SeedableRng;
8use std::ops::DerefMut;
9use std::sync::Arc;
10
11/// A data loader that can be used to iterate over a dataset in batches.
12pub struct BatchDataLoader<B: Backend, I, O> {
13    strategy: Box<dyn BatchStrategy<I>>,
14    dataset: Arc<dyn Dataset<I>>,
15    batcher: Arc<dyn Batcher<B, I, O>>,
16    device: B::Device,
17    rng: Option<Arc<spin::Mutex<rand::rngs::StdRng>>>,
18    batch_reading: bool,
19}
20
21impl<B: Backend, I, O> Clone for BatchDataLoader<B, I, O> {
22    fn clone(&self) -> Self {
23        Self {
24            strategy: self.strategy.clone_dyn(),
25            dataset: self.dataset.clone(),
26            batcher: self.batcher.clone(),
27            device: self.device.clone(),
28            rng: self.rng.clone(),
29            batch_reading: self.batch_reading,
30        }
31    }
32}
33
34impl<B: Backend, I, O> BatchDataLoader<B, I, O> {
35    /// Creates a new batch data loader.
36    ///
37    /// # Arguments
38    ///
39    /// * `strategy` - The batch strategy.
40    /// * `dataset` - The dataset.
41    /// * `batcher` - The batcher.
42    /// * `device`  - The device to use when loading a batch.
43    /// * `rng`     - The rng determining if the dataset is shuffled each time a dataloader
44    ///   iterator is created.
45    ///
46    /// # Returns
47    ///
48    /// The batch data loader.
49    pub fn new(
50        strategy: Box<dyn BatchStrategy<I>>,
51        dataset: Arc<dyn Dataset<I>>,
52        batcher: Arc<dyn Batcher<B, I, O>>,
53        device: B::Device,
54        rng: Option<rand::rngs::StdRng>,
55    ) -> Self {
56        Self {
57            strategy,
58            dataset,
59            batcher,
60            device,
61            rng: rng.map(|rng| Arc::new(spin::Mutex::new(rng))),
62            batch_reading: false,
63        }
64    }
65
66    /// Enables `Dataset::get_many` reads using the strategy's batch size (default: false).
67    ///
68    /// Unknown or zero batch sizes read one item at a time. Items are still passed
69    /// to the strategy individually; unread items remain buffered after an early
70    /// batch. Missing items or a mismatched result length panic without rereading.
71    pub fn batch_reading(mut self, enabled: bool) -> Self {
72        self.set_batch_reading(enabled);
73        self
74    }
75
76    pub(super) fn set_batch_reading(&mut self, enabled: bool) {
77        self.batch_reading = enabled;
78    }
79}
80
81/// A data loader iterator that can be used to iterate over a data loader.
82struct BatchDataloaderIterator<B: Backend, I, O> {
83    current_index: usize,
84    strategy: Box<dyn BatchStrategy<I>>,
85    dataset: Arc<dyn Dataset<I>>,
86    batcher: Arc<dyn Batcher<B, I, O>>,
87    device: B::Device,
88    batch_reading: bool,
89    pending: std::vec::IntoIter<I>,
90    read_indices: Vec<usize>,
91    read_failed: bool,
92}
93
94impl<B, I, O> DataLoader<B, O> for BatchDataLoader<B, I, O>
95where
96    B: Backend,
97    I: Send + Sync + Clone + 'static,
98    O: Send + 'static,
99{
100    fn iter<'a>(&'a self) -> Box<dyn DataLoaderIterator<O> + 'a> {
101        // When starting a new iteration, we first check if the dataloader was created with an rng,
102        // implying that we should shuffle the dataset beforehand, while advancing the current
103        // rng to ensure that each new iteration shuffles the dataset differently.
104        let dataset = match &self.rng {
105            Some(rng) => Arc::new(ShuffledDataset::new(
106                self.dataset.clone(),
107                rng.lock().deref_mut(),
108            )),
109            None => self.dataset.clone(),
110        };
111        Box::new(BatchDataloaderIterator::new(
112            self.strategy.clone_dyn(),
113            dataset,
114            self.batcher.clone(),
115            self.device.clone(),
116            self.batch_reading,
117        ))
118    }
119
120    fn num_items(&self) -> usize {
121        self.dataset.len()
122    }
123
124    fn to_device(&self, device: &B::Device) -> Arc<dyn DataLoader<B, O>> {
125        let rng = self.rng.as_ref().map(|rng| {
126            let mut rng = rng.lock();
127            rng.fork()
128        });
129        Arc::new(
130            Self::new(
131                self.strategy.clone_dyn(),
132                self.dataset.clone(),
133                self.batcher.clone(),
134                device.clone(),
135                rng,
136            )
137            .batch_reading(self.batch_reading),
138        )
139    }
140
141    fn slice(&self, start: usize, end: usize) -> Arc<dyn DataLoader<B, O>> {
142        let rng = self.rng.as_ref().map(|rng| {
143            let mut rng = rng.lock();
144            rng.fork()
145        });
146        let dataloader = Self::new(
147            self.strategy.clone_dyn(),
148            Arc::new(PartialDataset::new(self.dataset.clone(), start, end)),
149            self.batcher.clone(),
150            self.device.clone(),
151            rng,
152        )
153        .batch_reading(self.batch_reading);
154        Arc::new(dataloader)
155    }
156}
157
158impl<B: Backend, I, O> BatchDataloaderIterator<B, I, O> {
159    /// Creates a new batch data loader iterator.
160    ///
161    /// # Arguments
162    ///
163    /// * `strategy` - The batch strategy.
164    /// * `dataset` - The dataset.
165    /// * `batcher` - The batcher.
166    /// * `device`  - The device to use when loading a batch.
167    ///
168    /// # Returns
169    ///
170    /// The batch data loader iterator.
171    pub fn new(
172        strategy: Box<dyn BatchStrategy<I>>,
173        dataset: Arc<dyn Dataset<I>>,
174        batcher: Arc<dyn Batcher<B, I, O>>,
175        device: B::Device,
176        batch_reading: bool,
177    ) -> Self {
178        BatchDataloaderIterator {
179            current_index: 0,
180            strategy,
181            dataset,
182            batcher,
183            device,
184            batch_reading,
185            pending: Vec::new().into_iter(),
186            read_indices: Vec::new(),
187            read_failed: false,
188        }
189    }
190
191    fn next_item(&mut self) -> Option<I> {
192        if !self.batch_reading {
193            return self.dataset.get(self.current_index);
194        }
195
196        assert!(
197            !self.read_failed,
198            "cannot resume a failed batch dataset read"
199        );
200        if let Some(item) = self.pending.next() {
201            return Some(item);
202        }
203
204        let len = self.dataset.len();
205        if self.current_index >= len {
206            return None;
207        }
208        let count = self.strategy.batch_size().unwrap_or(1).max(1)
209            .min(len - self.current_index);
210        self.read_indices.clear();
211        self.read_indices
212            .extend(self.current_index..self.current_index + count);
213
214        self.read_failed = true;
215        let items = self.dataset.get_many(&self.read_indices).unwrap_or_else(|| {
216            panic!(
217                "batch dataset read is missing an item in indices {:?}",
218                self.read_indices
219            )
220        });
221        assert_eq!(
222            items.len(),
223            count,
224            "batch dataset read returned the wrong number of items"
225        );
226        self.pending = items.into_iter();
227        self.read_failed = false;
228        self.pending.next()
229    }
230}
231
232impl<B: Backend, I, O> Iterator for BatchDataloaderIterator<B, I, O> {
233    type Item = O;
234
235    fn next(&mut self) -> Option<O> {
236        while let Some(item) = self.next_item() {
237            self.current_index += 1;
238            self.strategy.add(item);
239
240            if let Some(items) = self.strategy.batch(false) {
241                return Some(self.batcher.batch(items, &self.device));
242            }
243        }
244
245        if let Some(items) = self.strategy.batch(true) {
246            return Some(self.batcher.batch(items, &self.device));
247        }
248
249        None
250    }
251}
252
253impl<B: Backend, I, O> DataLoaderIterator<O> for BatchDataloaderIterator<B, I, O> {
254    fn progress(&self) -> Progress {
255        Progress::new(self.current_index, self.dataset.len())
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use std::collections::HashSet;
262
263    use super::*;
264    use crate::data::dataloader::FixBatchStrategy;
265    use crate::data::dataloader::batcher::TestBatcher;
266    use crate::data::dataset::FakeDataset;
267
268    #[test]
269    fn test_batch_dataloader() {
270        let batcher = Arc::new(TestBatcher::new());
271        let dataset = Arc::new(FakeDataset::<String>::new(27));
272        let dataloader = BatchDataLoader::new(
273            Box::new(FixBatchStrategy::new(5)),
274            dataset.clone(),
275            batcher,
276            Default::default(),
277            None,
278        );
279
280        let mut items_dataset = HashSet::new();
281        let mut items_dataloader = HashSet::new();
282
283        for item in dataset.iter() {
284            items_dataset.insert(item);
285        }
286
287        for items in dataloader.iter() {
288            for item in items {
289                items_dataloader.insert(item);
290            }
291        }
292
293        assert_eq!(items_dataset, items_dataloader);
294    }
295
296    #[test]
297    fn test_batch_dataloader_slice() {
298        let batcher = Arc::new(TestBatcher::new());
299        let dataset = Arc::new(FakeDataset::<String>::new(27));
300        let dataloader = BatchDataLoader::new(
301            Box::new(FixBatchStrategy::new(5)),
302            dataset.clone(),
303            batcher,
304            Default::default(),
305            None,
306        );
307        let dataloader_slice = dataloader.slice(5, 15);
308
309        let mut items_dataloader = HashSet::new();
310        let mut items_dataloader_slice = HashSet::new();
311
312        let mut idx = 0;
313        for items in dataloader.iter() {
314            for item in items {
315                if (5..15).contains(&idx) {
316                    items_dataloader.insert(item);
317                }
318                idx += 1;
319            }
320        }
321
322        for items in dataloader_slice.iter() {
323            for item in items {
324                items_dataloader_slice.insert(item);
325            }
326        }
327
328        assert_eq!(items_dataloader, items_dataloader_slice);
329    }
330}