1use burn_dataset::Dataset;
2use burn_dataset::transform::PartialDataset;
3use burn_tensor::Device;
4use rand::distr::{Distribution, StandardUniform};
5use rand::rngs::StdRng;
6use rand::{Rng, SeedableRng};
7
8use super::batcher::Batcher;
9use super::{BatchDataLoader, BatchStrategy, DataLoader, DataLoaderIterator, Progress};
10use std::sync::{Arc, OnceLock, mpsc, mpsc::SyncSender};
11use std::thread;
12
13const MAX_QUEUED_ITEMS: usize = 100;
14
15type RngSeed = <StdRng as SeedableRng>::Seed;
16
17pub struct MultiThreadDataLoader<I, O> {
19 strategy: Box<dyn BatchStrategy<I>>,
21 dataset: Arc<dyn Dataset<I>>,
22 batcher: Arc<dyn Batcher<I, O>>,
23 device: Device,
24 seed: Option<RngSeed>,
25 num_threads: usize,
26
27 dataloaders: OnceLock<Vec<BatchDataLoader<I, O>>>,
29
30 workers: OnceLock<WorkerPool<O>>,
33}
34
35#[derive(Debug)]
37pub enum Message<O> {
38 Batch(usize, O, Progress),
40
41 Done,
43
44 Error(usize, String),
46}
47
48struct MultiThreadsDataloaderIterator<O> {
49 num_done: usize,
50 num_workers: usize,
51 receiver: mpsc::Receiver<Message<O>>,
52 progresses: Vec<Progress>,
53}
54
55type WorkerCommand<O> = SyncSender<Message<O>>;
57
58struct WorkerPool<O> {
59 senders: Vec<mpsc::Sender<WorkerCommand<O>>>,
61 handles: Vec<thread::JoinHandle<()>>,
62 item_counts: Vec<usize>,
63}
64
65impl<O> Drop for WorkerPool<O> {
66 fn drop(&mut self) {
67 self.senders.clear();
69 for handle in self.handles.drain(..) {
70 let _ = handle.join();
71 }
72 }
73}
74
75impl<I, O> MultiThreadDataLoader<I, O>
76where
77 I: Send + Sync + Clone + 'static,
78 O: Send + 'static,
79{
80 pub fn new(
96 strategy: Box<dyn BatchStrategy<I>>,
97 dataset: Arc<dyn Dataset<I>>,
98 batcher: Arc<dyn Batcher<I, O>>,
99 num_threads: usize,
100 device: Device,
101 rng: Option<rand::rngs::StdRng>,
102 ) -> Self {
103 let mut seed = None;
104 if let Some(mut rng) = rng {
105 let mut s = RngSeed::default();
108 rng.fill_bytes(&mut s);
109
110 seed = Some(s);
111 }
112 Self::from_seed(strategy, dataset, batcher, num_threads, device, seed)
113 }
114
115 fn from_seed(
116 strategy: Box<dyn BatchStrategy<I>>,
117 dataset: Arc<dyn Dataset<I>>,
118 batcher: Arc<dyn Batcher<I, O>>,
119 num_threads: usize,
120 device: Device,
121 seed: Option<RngSeed>,
122 ) -> Self {
123 Self {
124 strategy,
125 dataset,
126 batcher,
127 num_threads,
128 device,
129 seed,
130 dataloaders: OnceLock::new(),
131 workers: OnceLock::new(),
132 }
133 }
134
135 fn initialize(&self) -> &[BatchDataLoader<I, O>] {
137 self.dataloaders
138 .get_or_init(|| {
139 let mut dataset = self.dataset.clone();
140 if let Some(seed) = self.seed.as_ref() {
141 let mut rng = StdRng::from_seed(*seed);
144 dataset = Arc::new(burn_dataset::transform::ShuffledDataset::new(
145 dataset, &mut rng,
146 ));
147 }
148
149 let datasets = match self.strategy.batch_size() {
150 Some(batch_size) => {
151 PartialDataset::split_chunks(dataset, self.num_threads, batch_size)
152 }
153 None => PartialDataset::split(dataset, self.num_threads),
154 };
155
156 let mut rng = self.seed.map(StdRng::from_seed);
158 let rngs = (0..self.num_threads).map(|_| {
159 rng.as_mut().map(|rng| {
160 StdRng::seed_from_u64(Distribution::sample(&StandardUniform, rng))
161 })
162 });
163
164 datasets
165 .into_iter()
166 .zip(rngs)
167 .map(|(dataset, rng)| {
168 let strategy = self.strategy.clone_dyn();
169 BatchDataLoader::new(
170 strategy,
171 Arc::new(dataset),
172 self.batcher.clone(),
173 self.device.clone(),
174 rng,
175 )
176 })
177 .collect()
178 })
179 .as_ref()
180 }
181
182 fn workers(&self) -> &WorkerPool<O> {
184 self.workers.get_or_init(|| {
185 let dataloaders = self.initialize();
186 let item_counts: Vec<usize> = dataloaders.iter().map(|d| d.num_items()).collect();
187
188 let mut senders = Vec::with_capacity(dataloaders.len());
189 let mut handles = Vec::with_capacity(dataloaders.len());
190
191 for (index, dataloader) in dataloaders.iter().enumerate() {
192 let dataloader = dataloader.clone();
193 let (command_sender, command_receiver) = mpsc::channel::<WorkerCommand<O>>();
194
195 let handle = thread::Builder::new()
196 .name(std::format!("dataloader-{index}"))
197 .spawn(move || {
198 while let Ok(sender) = command_receiver.recv() {
199 let mut iterator = dataloader.iter();
200 loop {
201 match iterator.next() {
202 Some(Ok(item)) => {
203 let progress = iterator.progress();
204
205 if sender
206 .send(Message::Batch(index, item, progress))
207 .is_err()
208 {
209 break;
210 }
211 }
212 None => break,
213 Some(Err(dataset_err)) => {
214 sender
215 .send(Message::Error(index, dataset_err.to_string()))
216 .ok();
217 break;
218 }
219 }
220 }
221 sender.send(Message::Done).ok();
222 }
223 })
224 .unwrap();
225
226 senders.push(command_sender);
227 handles.push(handle);
228 }
229
230 WorkerPool {
231 senders,
232 handles,
233 item_counts,
234 }
235 })
236 }
237}
238
239impl<I, O> DataLoader<O> for MultiThreadDataLoader<I, O>
240where
241 I: Send + Sync + Clone + 'static,
242 O: Send + 'static + std::fmt::Debug,
243{
244 fn iter<'a>(&'a self) -> Box<dyn DataLoaderIterator<O> + 'a> {
245 let workers = self.workers();
246
247 let (sender, receiver) = mpsc::sync_channel::<Message<O>>(MAX_QUEUED_ITEMS);
248 let unit: Option<String> = Some("items".to_string());
249
250 let mut progresses = Vec::with_capacity(workers.senders.len());
251 for (command_sender, &num_items) in workers.senders.iter().zip(workers.item_counts.iter()) {
252 progresses.push(Progress::new(0, num_items, unit.clone()));
253 command_sender
254 .send(sender.clone())
255 .expect("Dataloader worker thread should be alive");
256 }
257 let num_workers = workers.senders.len();
258
259 drop(sender);
261
262 Box::new(MultiThreadsDataloaderIterator::new(
263 receiver,
264 num_workers,
265 progresses,
266 ))
267 }
268
269 fn num_items(&self) -> usize {
270 self.dataset.len()
273 }
274
275 fn to_device(&self, device: &Device) -> Arc<dyn DataLoader<O>> {
276 Arc::new(Self::from_seed(
277 self.strategy.clone_dyn(),
278 self.dataset.clone(),
279 self.batcher.clone(),
280 self.num_threads,
281 device.clone(),
282 self.seed,
283 ))
284 }
285
286 fn slice(&self, start: usize, end: usize) -> Arc<dyn DataLoader<O>> {
287 let dataloader = Self::from_seed(
288 self.strategy.clone_dyn(),
289 Arc::new(PartialDataset::new(self.dataset.clone(), start, end)),
290 self.batcher.clone(),
291 self.num_threads,
292 self.device.clone(),
293 self.seed,
294 );
295 Arc::new(dataloader)
296 }
297}
298
299impl<O> MultiThreadsDataloaderIterator<O> {
300 pub fn new(
301 receiver: mpsc::Receiver<Message<O>>,
302 num_workers: usize,
303 progresses: Vec<Progress>,
304 ) -> Self {
305 MultiThreadsDataloaderIterator {
306 num_done: 0,
307 num_workers,
308 receiver,
309 progresses,
310 }
311 }
312}
313impl<O: std::fmt::Debug> DataLoaderIterator<O> for MultiThreadsDataloaderIterator<O> {
314 fn progress(&self) -> Progress {
315 let mut items_total = 0;
316 let mut items_processed = 0;
317 let unit: Option<String> = Some("items".to_string());
318
319 for progress in self.progresses.iter() {
320 items_total += progress.items_total;
321 items_processed += progress.items_processed;
322 }
323
324 Progress::new(items_processed, items_total, unit)
325 }
326}
327
328impl<O: std::fmt::Debug> Iterator for MultiThreadsDataloaderIterator<O> {
329 type Item = Result<O, burn_dataset::DatasetError>;
330
331 fn next(&mut self) -> Option<Self::Item> {
332 if self.num_workers == 0 {
333 return None;
334 }
335
336 loop {
337 match self.receiver.recv() {
338 Ok(Message::Batch(index, item, progress)) => {
339 if let Some(current) = self.progresses.get_mut(index) {
340 *current = progress;
341 }
342 return Some(Ok(item));
343 }
344 Ok(Message::Done) => {
345 self.num_done += 1;
346 if self.num_done == self.num_workers {
347 return None;
349 }
350 }
351 Ok(Message::Error(index, msg)) => {
352 return Some(Err(burn_dataset::DatasetError::new(std::io::Error::other(
353 format!("dataloader worker {index} failed: {msg}"),
354 ))));
355 }
356 Err(_) => return None,
357 }
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::data::dataloader::FixBatchStrategy;
366 use crate::data::dataloader::batcher::TestBatcher;
367 use crate::data::dataset::FakeDataset;
368 use burn_dataset::DatasetError;
369 use burn_dataset::InMemDataset;
370 use std::collections::HashSet;
371 use std::sync::mpsc;
372 use std::time::Duration;
373
374 struct FlakyDataset {
377 len: usize,
378 fail_at: usize,
379 }
380
381 impl Dataset<usize> for FlakyDataset {
382 fn get(&self, index: usize) -> Result<usize, DatasetError> {
383 assert!(index < self.len, "index out of bounds");
384 if index == self.fail_at {
385 return Err(DatasetError::new(std::io::Error::other(
386 "simulated dataset failure",
387 )));
388 }
389 Ok(index)
390 }
391
392 fn len(&self) -> usize {
393 self.len
394 }
395 }
396
397 #[test]
398 fn test_multi_thread_batch_dataloader() {
399 let batcher = Arc::new(TestBatcher::new());
400 let dataset = Arc::new(FakeDataset::<String>::new(27));
401 let dataloader_single_thread = BatchDataLoader::new(
402 Box::new(FixBatchStrategy::new(5)),
403 dataset.clone(),
404 batcher.clone(),
405 Default::default(),
406 None,
407 );
408 let dataloader_multi_thread = MultiThreadDataLoader::new(
409 Box::new(FixBatchStrategy::new(5)),
410 dataset,
411 batcher,
412 4,
413 Default::default(),
414 None,
415 );
416
417 let mut items_single_thread = HashSet::new();
418 let mut items_multi_thread = HashSet::new();
419
420 for items in dataloader_single_thread.iter().map(Result::unwrap) {
421 for item in items {
422 items_single_thread.insert(item);
423 }
424 }
425
426 for items in dataloader_multi_thread.iter().map(Result::unwrap) {
427 for item in items {
428 items_multi_thread.insert(item);
429 }
430 }
431
432 assert_eq!(items_single_thread, items_multi_thread);
433 }
434
435 #[test]
436 fn test_multi_thread_batch_dataloader_shuffle() {
437 let num_classes = 2;
438 let class_size = 100;
439 let batch_size = 10;
440
441 let mut items = Vec::new();
443 for class in 0..num_classes {
444 items.extend(vec![class; class_size]);
445 }
446
447 {
448 let dataset = Arc::new(InMemDataset::new(items.clone()));
450 let batcher = Arc::new(TestBatcher::new());
451
452 let loader = MultiThreadDataLoader::new(
453 Box::new(FixBatchStrategy::new(batch_size)),
454 dataset,
455 batcher,
456 num_classes,
457 Default::default(),
458 None,
460 );
461
462 for batch in loader.iter().map(Result::unwrap) {
463 let mut batch_items = HashSet::new();
464 for item in batch {
465 batch_items.insert(item);
466 }
467
468 assert_eq!(batch_items.len(), 1);
470 }
471 }
472
473 {
474 let dataset = Arc::new(InMemDataset::new(items.clone()));
476 let batcher = Arc::new(TestBatcher::new());
477
478 let loader = MultiThreadDataLoader::new(
479 Box::new(FixBatchStrategy::new(batch_size)),
480 dataset.clone(),
481 batcher.clone(),
482 num_classes,
483 Default::default(),
484 Some(StdRng::seed_from_u64(42)),
486 );
487
488 for batch in loader.iter().map(Result::unwrap) {
489 let mut batch_items = HashSet::new();
490 for item in batch {
491 batch_items.insert(item);
492 }
493
494 assert_eq!(batch_items.len(), num_classes);
496 }
497 }
498 }
499
500 #[test]
501 fn test_multi_thread_batch_dataloader_incomplete_batches() {
502 let batcher = Arc::new(TestBatcher::new());
503 let dataset = Arc::new(FakeDataset::<String>::new(27));
504 let dataloader_single_thread = BatchDataLoader::new(
505 Box::new(FixBatchStrategy::new(5)),
506 dataset.clone(),
507 batcher.clone(),
508 Default::default(),
509 None,
510 );
511 let dataloader_multi_thread = MultiThreadDataLoader::new(
512 Box::new(FixBatchStrategy::new(5)),
513 dataset,
514 batcher,
515 4,
516 Default::default(),
517 None,
518 );
519
520 let mut items_single_thread = HashSet::new();
521 let mut items_multi_thread = HashSet::new();
522
523 let mut single_thread_cnt = 0;
524 let mut multi_thread_cnt = 0;
525 for items in dataloader_single_thread.iter().map(Result::unwrap) {
526 items_single_thread.insert(items);
527 single_thread_cnt += 1;
528 }
529
530 for items in dataloader_multi_thread.iter().map(Result::unwrap) {
531 items_multi_thread.insert(items);
532 multi_thread_cnt += 1;
533 }
534
535 assert_eq!(single_thread_cnt, multi_thread_cnt);
536 assert_eq!(items_single_thread, items_multi_thread);
537 }
538
539 #[test]
541 fn test_multi_thread_batch_dataloader_multiple_epochs() {
542 let batcher = Arc::new(TestBatcher::new());
543 let dataset = Arc::new(FakeDataset::<String>::new(27));
544
545 let expected: HashSet<_> = dataset.iter().map(Result::unwrap).collect();
546
547 let dataloader = MultiThreadDataLoader::new(
548 Box::new(FixBatchStrategy::new(5)),
549 dataset,
550 batcher,
551 4,
552 Default::default(),
553 None,
554 );
555
556 for _epoch in 0..3 {
557 let mut items = HashSet::new();
558 for batch in dataloader.iter().map(Result::unwrap) {
559 for item in batch {
560 items.insert(item);
561 }
562 }
563 assert_eq!(items, expected);
564 }
565 }
566
567 #[test]
569 fn test_multi_thread_batch_dataloader_resumes_after_early_drop() {
570 let batcher = Arc::new(TestBatcher::new());
571 let dataset = Arc::new(FakeDataset::<String>::new(27));
572
573 let expected: HashSet<_> = dataset.iter().map(Result::unwrap).collect();
574
575 let dataloader = MultiThreadDataLoader::new(
576 Box::new(FixBatchStrategy::new(5)),
577 dataset,
578 batcher,
579 4,
580 Default::default(),
581 None,
582 );
583
584 {
586 let mut iterator = dataloader.iter();
587 let _ = iterator.next();
588 }
589
590 let mut items = HashSet::new();
591 for batch in dataloader.iter().map(Result::unwrap) {
592 for item in batch {
593 items.insert(item);
594 }
595 }
596 assert_eq!(items, expected);
597 }
598
599 #[test]
600 fn test_multi_thread_batch_dataloader_propagates_worker_error_instead_of_hanging() {
601 let batcher = Arc::new(TestBatcher::new());
602 let dataset = Arc::new(FlakyDataset {
603 len: 40,
604 fail_at: 20,
605 });
606 let dataloader = MultiThreadDataLoader::new(
607 Box::new(FixBatchStrategy::new(1)),
608 dataset,
609 batcher,
610 4,
611 Default::default(),
612 None,
613 );
614
615 let (done_tx, done_rx) = mpsc::channel();
616 let handle = thread::spawn(move || {
617 let saw_error = dataloader.iter().any(|batch| batch.is_err());
618 done_tx.send(saw_error).ok();
619 });
620
621 let saw_error = done_rx
622 .recv_timeout(Duration::from_secs(10))
623 .expect("dataloader hung instead of reporting the worker error");
624 assert!(
625 saw_error,
626 "expected the dataloader to yield an Err on a worker error"
627 );
628 let _ = handle.join();
629 }
630}