1use ruda_dataset::Dataset;
2use ruda_dataset::transform::PartialDataset;
3use ruda_tensor::api::backend::Backend;
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::{
11 Arc, Mutex, OnceLock,
12 atomic::{AtomicBool, Ordering},
13 mpsc,
14};
15use std::thread;
16
17const MAX_QUEUED_ITEMS: usize = 100;
18
19type RngSeed = <StdRng as SeedableRng>::Seed;
20
21pub struct MultiThreadDataLoader<B: Backend, I, O> {
27 strategy: Box<dyn BatchStrategy<I>>,
29 dataset: Arc<dyn Dataset<I>>,
30 batcher: Arc<dyn Batcher<B, I, O>>,
31 device: B::Device,
32 seed: Option<RngSeed>,
33 num_threads: usize,
34 reuse_workers: bool,
35 batch_reading: bool,
36
37 dataloaders: OnceLock<Vec<BatchDataLoader<B, I, O>>>,
39 worker_pool: OnceLock<Arc<Mutex<WorkerPoolState<O>>>>,
40}
41
42#[derive(Debug, crate::record::Record)]
44pub enum Message<O> {
45 Batch(usize, O, Progress),
47
48 Done,
50}
51
52struct MultiThreadsDataloaderIterator<O> {
53 num_done: usize,
54 num_workers: usize,
55 workers: Vec<thread::JoinHandle<()>>,
56 reused_workers: Option<WorkerEpoch<O>>,
57 receiver: Option<mpsc::Receiver<Message<O>>>,
58 progresses: Vec<Progress>,
59 items_total: usize,
60 items_processed: usize,
61 cancelled: Arc<AtomicBool>,
62}
63
64enum WorkerPoolState<O> {
65 Uninitialized,
66 Idle(WorkerPool<O>),
67 InUse,
68}
69
70struct WorkerJob<O> {
71 sender: mpsc::SyncSender<Message<O>>,
72 cancelled: Arc<AtomicBool>,
73 finished: mpsc::Sender<bool>,
74 load: Box<dyn FnOnce(&mpsc::SyncSender<Message<O>>, &AtomicBool) + Send>,
75}
76
77struct WorkerCompletion(mpsc::Sender<bool>);
78
79impl Drop for WorkerCompletion {
80 fn drop(&mut self) {
81 let _ = self.0.send(thread::panicking());
84 }
85}
86
87struct WorkerPool<O> {
88 senders: Vec<mpsc::Sender<WorkerJob<O>>>,
89 workers: Vec<thread::JoinHandle<()>>,
90}
91
92impl<O> WorkerPool<O> {
93 fn shutdown(&mut self) -> thread::Result<()> {
94 self.senders.clear();
95 let mut result = Ok(());
96 for worker in self.workers.drain(..) {
97 if let Err(payload) = worker.join() {
98 if result.is_ok() {
99 result = Err(payload);
100 }
101 }
102 }
103 result
104 }
105}
106
107impl<O> Drop for WorkerPool<O> {
108 fn drop(&mut self) {
109 let _ = self.shutdown();
110 }
111}
112
113struct WorkerLease<O> {
114 pool: Option<WorkerPool<O>>,
115 cache: Option<Arc<Mutex<WorkerPoolState<O>>>>,
116}
117
118impl<O> WorkerLease<O> {
119 fn release(mut self) {
120 let pool = self.pool.take().expect("leased worker pool");
121 let cache = self.cache.take().expect("worker pool cache");
122 *cache.lock().unwrap_or_else(|error| error.into_inner()) = WorkerPoolState::Idle(pool);
123 }
124}
125
126impl<O> Drop for WorkerLease<O> {
127 fn drop(&mut self) {
128 if let Some(cache) = self.cache.take() {
129 drop(self.pool.take());
132 *cache.lock().unwrap_or_else(|error| error.into_inner()) =
133 WorkerPoolState::Uninitialized;
134 }
135 }
136}
137
138struct WorkerEpoch<O> {
139 lease: WorkerLease<O>,
140 finished: mpsc::Receiver<bool>,
141 dispatched: usize,
142 failed: bool,
143}
144
145impl<O> WorkerEpoch<O> {
146 fn finish(mut self) -> thread::Result<()> {
147 for _ in 0..self.dispatched {
148 match self.finished.recv() {
149 Ok(panicked) => self.failed |= panicked,
150 Err(_) => {
151 self.failed = true;
152 break;
153 }
154 }
155 }
156 if self.failed {
157 let result = self
158 .lease
159 .pool
160 .as_mut()
161 .expect("leased worker pool")
162 .shutdown();
163 return match result {
164 Err(payload) => Err(payload),
165 Ok(()) => Err(Box::new(String::from("data loader worker failed to complete"))),
166 };
167 }
168 self.lease.release();
169 Ok(())
170 }
171}
172
173fn run_worker<B: Backend, I, O>(
174 dataloader: &BatchDataLoader<B, I, O>,
175 index: usize,
176 sender: &mpsc::SyncSender<Message<O>>,
177 cancelled: &AtomicBool,
178) where
179 I: Send + Sync + Clone + 'static,
180 O: Send + 'static,
181{
182 if cancelled.load(Ordering::Relaxed) {
183 return;
184 }
185 let mut iterator = dataloader.iter();
186 while !cancelled.load(Ordering::Relaxed) {
187 let Some(item) = iterator.next() else {
188 break;
189 };
190 let progress = iterator.progress();
191 if sender.send(Message::Batch(index, item, progress)).is_err() {
192 return;
193 }
194 }
195 sender.send(Message::Done).ok();
196}
197
198impl<B: Backend, I, O> MultiThreadDataLoader<B, I, O>
199where
200 I: Send + Sync + Clone + 'static,
201 O: Send + 'static,
202{
203 pub fn new(
219 strategy: Box<dyn BatchStrategy<I>>,
220 dataset: Arc<dyn Dataset<I>>,
221 batcher: Arc<dyn Batcher<B, I, O>>,
222 num_threads: usize,
223 device: B::Device,
224 rng: Option<rand::rngs::StdRng>,
225 ) -> Self {
226 let mut seed = None;
227 if let Some(mut rng) = rng {
228 let mut s = RngSeed::default();
231 rng.fill_bytes(&mut s);
232
233 seed = Some(s);
234 }
235 Self::from_seed(strategy, dataset, batcher, num_threads, device, seed)
236 }
237
238 fn from_seed(
239 strategy: Box<dyn BatchStrategy<I>>,
240 dataset: Arc<dyn Dataset<I>>,
241 batcher: Arc<dyn Batcher<B, I, O>>,
242 num_threads: usize,
243 device: B::Device,
244 seed: Option<RngSeed>,
245 ) -> Self {
246 Self {
247 strategy,
248 dataset,
249 batcher,
250 num_threads,
251 device,
252 seed,
253 dataloaders: OnceLock::new(),
254 reuse_workers: false,
255 batch_reading: false,
256 worker_pool: OnceLock::new(),
257 }
258 }
259
260 pub fn reuse_workers(mut self, enabled: bool) -> Self {
267 self.reuse_workers = enabled;
268 if !enabled {
269 drop(self.worker_pool.take());
270 }
271 self
272 }
273
274 pub fn batch_reading(mut self, enabled: bool) -> Self {
279 self.batch_reading = enabled;
280 if let Some(dataloaders) = self.dataloaders.get_mut() {
281 for dataloader in dataloaders {
282 dataloader.set_batch_reading(enabled);
283 }
284 }
285 self
286 }
287
288 fn lease_workers(&self, num_workers: usize) -> Option<WorkerLease<O>> {
289 if !self.reuse_workers {
290 return None;
291 }
292 let worker_pool = self
293 .worker_pool
294 .get_or_init(|| Arc::new(Mutex::new(WorkerPoolState::Uninitialized)));
295 let pool = {
296 let mut cache = worker_pool
297 .lock()
298 .unwrap_or_else(|error| error.into_inner());
299 if matches!(*cache, WorkerPoolState::InUse) {
300 return None;
301 }
302 match std::mem::replace(&mut *cache, WorkerPoolState::InUse) {
303 WorkerPoolState::Idle(pool) => Some(pool),
304 WorkerPoolState::Uninitialized => None,
305 WorkerPoolState::InUse => unreachable!(),
306 }
307 };
308 let mut lease = WorkerLease {
309 pool,
310 cache: Some(worker_pool.clone()),
311 };
312 if lease.pool.is_none() {
313 let mut pool = WorkerPool {
314 senders: Vec::with_capacity(num_workers),
315 workers: Vec::with_capacity(num_workers),
316 };
317 for index in 0..num_workers {
318 let (sender, receiver) = mpsc::channel::<WorkerJob<O>>();
319 let worker = thread::Builder::new()
320 .name(std::format!("dataloader-{index}"))
321 .spawn(move || {
322 while let Ok(job) = receiver.recv() {
323 let completion = WorkerCompletion(job.finished);
324 (job.load)(&job.sender, &job.cancelled);
325 drop(job.sender);
326 drop(job.cancelled);
327 drop(completion);
328 }
329 })
330 .expect("failed to spawn reusable data loader worker");
331 pool.senders.push(sender);
332 pool.workers.push(worker);
333 }
334 lease.pool = Some(pool);
335 }
336 Some(lease)
337 }
338
339 fn initialize(&self) -> &[BatchDataLoader<B, I, O>] {
341 self.dataloaders
342 .get_or_init(|| {
343 let mut dataset = self.dataset.clone();
344 if let Some(seed) = self.seed.as_ref() {
345 let mut rng = StdRng::from_seed(*seed);
348 dataset = Arc::new(ruda_dataset::transform::ShuffledDataset::new(
349 dataset, &mut rng,
350 ));
351 }
352
353 let datasets = match self.strategy.batch_size() {
354 Some(batch_size) => {
355 let split_size = if self.batch_reading { batch_size.max(1) } else { batch_size };
356 PartialDataset::split_chunks(dataset, self.num_threads, split_size)
357 }
358 None => PartialDataset::split(dataset, self.num_threads),
359 };
360
361 let mut rng = self.seed.map(StdRng::from_seed);
363 let rngs = (0..self.num_threads).map(|_| {
364 rng.as_mut().map(|rng| {
365 StdRng::seed_from_u64(Distribution::sample(&StandardUniform, rng))
366 })
367 });
368
369 datasets
370 .into_iter()
371 .zip(rngs)
372 .map(|(dataset, rng)| {
373 let strategy = self.strategy.clone_dyn();
374 BatchDataLoader::new(
375 strategy,
376 Arc::new(dataset),
377 self.batcher.clone(),
378 self.device.clone(),
379 rng,
380 )
381 .batch_reading(self.batch_reading)
382 })
383 .collect()
384 })
385 .as_ref()
386 }
387}
388
389impl<B: Backend, I, O> DataLoader<B, O> for MultiThreadDataLoader<B, I, O>
390where
391 I: Send + Sync + Clone + 'static,
392 O: Send + 'static + std::fmt::Debug,
393{
394 fn iter<'a>(&'a self) -> Box<dyn DataLoaderIterator<O> + 'a> {
395 let dataloaders = self.initialize();
397
398 let (sender, receiver) = mpsc::sync_channel::<Message<O>>(MAX_QUEUED_ITEMS);
399
400 let progresses = dataloaders
401 .iter()
402 .map(|dataloader| Progress::new(0, dataloader.num_items()))
403 .collect();
404 let mut iterator = MultiThreadsDataloaderIterator::new(
405 receiver,
406 Vec::with_capacity(dataloaders.len()),
407 progresses,
408 );
409
410 if let Some(lease) = self.lease_workers(dataloaders.len()) {
411 let (finished, receiver) = mpsc::channel();
412 iterator.reused_workers = Some(WorkerEpoch {
413 lease,
414 finished: receiver,
415 dispatched: 0,
416 failed: true,
417 });
418 let epoch = iterator.reused_workers.as_mut().expect("worker epoch");
419 for (index, (worker, dataloader)) in epoch
420 .lease
421 .pool
422 .as_ref()
423 .expect("leased worker pool")
424 .senders
425 .iter()
426 .zip(dataloaders)
427 .enumerate()
428 {
429 let dataloader = dataloader.clone();
430 worker
431 .send(WorkerJob {
432 sender: sender.clone(),
433 cancelled: iterator.cancelled.clone(),
434 finished: finished.clone(),
435 load: Box::new(move |sender, cancelled| {
436 run_worker(&dataloader, index, sender, cancelled);
437 }),
438 })
439 .unwrap_or_else(|_| panic!("reusable data loader worker disconnected"));
440 epoch.dispatched += 1;
441 }
442 epoch.failed = false;
443 drop(finished);
444 } else {
445 for (index, dataloader) in dataloaders.iter().enumerate() {
446 let dataloader_cloned = dataloader.clone();
447 let sender_cloned = sender.clone();
448 let cancelled = iterator.cancelled.clone();
449 let worker = std::thread::Builder::new()
450 .name(std::format!("dataloader-{index}"))
451 .spawn(move || {
452 run_worker(&dataloader_cloned, index, &sender_cloned, &cancelled);
453 })
454 .expect("failed to spawn data loader worker");
455 iterator.workers.push(worker);
456 }
457 }
458 drop(sender);
459
460 Box::new(iterator)
461 }
462
463 fn num_items(&self) -> usize {
464 self.dataset.len()
467 }
468
469 fn to_device(&self, device: &B::Device) -> Arc<dyn DataLoader<B, O>> {
470 Arc::new(
471 Self::from_seed(
472 self.strategy.clone_dyn(),
473 self.dataset.clone(),
474 self.batcher.clone(),
475 self.num_threads,
476 device.clone(),
477 self.seed,
478 )
479 .reuse_workers(self.reuse_workers)
480 .batch_reading(self.batch_reading),
481 )
482 }
483
484 fn slice(&self, start: usize, end: usize) -> Arc<dyn DataLoader<B, O>> {
485 let dataloader = Self::from_seed(
486 self.strategy.clone_dyn(),
487 Arc::new(PartialDataset::new(self.dataset.clone(), start, end)),
488 self.batcher.clone(),
489 self.num_threads,
490 self.device.clone(),
491 self.seed,
492 )
493 .reuse_workers(self.reuse_workers)
494 .batch_reading(self.batch_reading);
495 Arc::new(dataloader)
496 }
497}
498
499impl<O> MultiThreadsDataloaderIterator<O> {
500 pub fn new(
501 receiver: mpsc::Receiver<Message<O>>,
502 workers: Vec<thread::JoinHandle<()>>,
503 progresses: Vec<Progress>,
504 ) -> Self {
505 MultiThreadsDataloaderIterator {
506 num_done: 0,
507 num_workers: progresses.len(),
508 workers,
509 reused_workers: None,
510 receiver: Some(receiver),
511 items_total: progresses.iter().map(|progress| progress.items_total).sum(),
512 items_processed: progresses.iter().map(|progress| progress.items_processed).sum(),
513 progresses,
514 cancelled: Arc::new(AtomicBool::new(false)),
515 }
516 }
517
518 fn shutdown(&mut self) -> thread::Result<()> {
519 self.cancelled.store(true, Ordering::Relaxed);
520 drop(self.receiver.take());
522 let mut result = match self.reused_workers.take() {
523 Some(epoch) => epoch.finish(),
524 None => Ok(()),
525 };
526 for worker in self.workers.drain(..) {
527 if let Err(payload) = worker.join() {
528 if result.is_ok() {
529 result = Err(payload);
530 }
531 }
532 }
533 result
534 }
535}
536
537impl<O> Drop for MultiThreadsDataloaderIterator<O> {
538 fn drop(&mut self) {
539 let _ = self.shutdown();
541 }
542}
543impl<O: std::fmt::Debug> DataLoaderIterator<O> for MultiThreadsDataloaderIterator<O> {
544 fn progress(&self) -> Progress {
545 Progress::new(self.items_processed, self.items_total)
546 }
547}
548
549impl<O: std::fmt::Debug> Iterator for MultiThreadsDataloaderIterator<O> {
550 type Item = O;
551
552 fn next(&mut self) -> Option<O> {
553 if self.receiver.is_none() || self.num_workers == 0 {
554 return None;
555 }
556
557 loop {
558 let item = match self.receiver.as_ref()?.recv() {
559 Ok(item) => item,
560 Err(_) => {
561 if let Err(payload) = self.shutdown() {
562 std::panic::resume_unwind(payload);
563 }
564 panic!("data loader workers disconnected before reporting completion");
565 }
566 };
567
568 match item {
569 Message::Batch(index, item, progress) => {
570 if let Some(current) = self.progresses.get_mut(index) {
571 self.items_processed =
572 self.items_processed - current.items_processed + progress.items_processed;
573 self.items_total =
574 self.items_total - current.items_total + progress.items_total;
575 *current = progress;
576 }
577 return Some(item);
578 }
579 Message::Done => {
580 self.num_done += 1;
581 }
582 };
583
584 if self.num_done == self.num_workers {
585 if let Err(payload) = self.shutdown() {
586 std::panic::resume_unwind(payload);
587 }
588 return None;
589 }
590 }
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::data::dataloader::FixBatchStrategy;
598 use crate::data::dataloader::batcher::TestBatcher;
599 use crate::data::dataset::FakeDataset;
600 use ruda_dataset::InMemDataset;
601 use std::collections::HashSet;
602
603 #[test]
604 fn test_multi_thread_batch_dataloader() {
605 let batcher = Arc::new(TestBatcher::new());
606 let dataset = Arc::new(FakeDataset::<String>::new(27));
607 let dataloader_single_thread = BatchDataLoader::new(
608 Box::new(FixBatchStrategy::new(5)),
609 dataset.clone(),
610 batcher.clone(),
611 Default::default(),
612 None,
613 );
614 let dataloader_multi_thread = MultiThreadDataLoader::new(
615 Box::new(FixBatchStrategy::new(5)),
616 dataset,
617 batcher,
618 4,
619 Default::default(),
620 None,
621 );
622
623 let mut items_single_thread = HashSet::new();
624 let mut items_multi_thread = HashSet::new();
625
626 for items in dataloader_single_thread.iter() {
627 for item in items {
628 items_single_thread.insert(item);
629 }
630 }
631
632 for items in dataloader_multi_thread.iter() {
633 for item in items {
634 items_multi_thread.insert(item);
635 }
636 }
637
638 assert_eq!(items_single_thread, items_multi_thread);
639 }
640
641 #[test]
642 fn test_multi_thread_batch_dataloader_shuffle() {
643 let num_classes = 2;
644 let class_size = 100;
645 let batch_size = 10;
646
647 let mut items = Vec::new();
649 for class in 0..num_classes {
650 items.extend(vec![class; class_size]);
651 }
652
653 {
654 let dataset = Arc::new(InMemDataset::new(items.clone()));
656 let batcher = Arc::new(TestBatcher::new());
657
658 let loader = MultiThreadDataLoader::new(
659 Box::new(FixBatchStrategy::new(batch_size)),
660 dataset,
661 batcher,
662 num_classes,
663 Default::default(),
664 None,
666 );
667
668 for batch in loader.iter() {
669 let mut batch_items = HashSet::new();
670 for item in batch {
671 batch_items.insert(item);
672 }
673
674 assert_eq!(batch_items.len(), 1);
676 }
677 }
678
679 {
680 let dataset = Arc::new(InMemDataset::new(items.clone()));
682 let batcher = Arc::new(TestBatcher::new());
683
684 let loader = MultiThreadDataLoader::new(
685 Box::new(FixBatchStrategy::new(batch_size)),
686 dataset.clone(),
687 batcher.clone(),
688 num_classes,
689 Default::default(),
690 Some(StdRng::seed_from_u64(42)),
692 );
693
694 for batch in loader.iter() {
695 let mut batch_items = HashSet::new();
696 for item in batch {
697 batch_items.insert(item);
698 }
699
700 assert_eq!(batch_items.len(), num_classes);
702 }
703 }
704 }
705
706 #[test]
707 fn test_multi_thread_batch_dataloader_incomplete_batches() {
708 let batcher = Arc::new(TestBatcher::new());
709 let dataset = Arc::new(FakeDataset::<String>::new(27));
710 let dataloader_single_thread = BatchDataLoader::new(
711 Box::new(FixBatchStrategy::new(5)),
712 dataset.clone(),
713 batcher.clone(),
714 Default::default(),
715 None,
716 );
717 let dataloader_multi_thread = MultiThreadDataLoader::new(
718 Box::new(FixBatchStrategy::new(5)),
719 dataset,
720 batcher,
721 4,
722 Default::default(),
723 None,
724 );
725
726 let mut items_single_thread = HashSet::new();
727 let mut items_multi_thread = HashSet::new();
728
729 let mut single_thread_cnt = 0;
730 let mut multi_thread_cnt = 0;
731 for items in dataloader_single_thread.iter() {
732 items_single_thread.insert(items);
733 single_thread_cnt += 1;
734 }
735
736 for items in dataloader_multi_thread.iter() {
737 items_multi_thread.insert(items);
738 multi_thread_cnt += 1;
739 }
740
741 assert_eq!(single_thread_cnt, multi_thread_cnt);
742 assert_eq!(items_single_thread, items_multi_thread);
743 }
744}