use std::sync::Arc;
#[cfg(feature = "python")]
use std::time::Instant;
#[cfg(feature = "python")]
use crossbeam_channel::RecvTimeoutError;
use crossbeam_channel::{Receiver, TryRecvError, unbounded};
use crate::{
collator::Collator,
dataset::Dataset,
error::{Error, Result},
loader::worker::{Epoch, WorkerMsg, process_batch},
};
fn forked() -> Error {
"this epoch was started before fork(); start a new one in this process (iter())".into()
}
struct ParallelCore<D, C>
where
D: Dataset,
C: Collator<D::Item>,
{
epoch: Arc<Epoch<D, C>>,
result_rx: Receiver<WorkerMsg<C::Batch>>,
reorder: Vec<Option<Result<C::Batch>>>,
next_out: usize,
pid: u32,
}
impl<D, C> ParallelCore<D, C>
where
D: Dataset,
C: Collator<D::Item>,
C::Batch: Send + 'static,
{
fn new(
dataset: &Arc<D>,
collator: &Arc<C>,
pool: &Arc<rayon::ThreadPool>,
items_in_parallel: usize,
num_workers: usize,
prefetch_depth: usize,
chunks: Vec<Vec<usize>>,
) -> Self {
let (result_tx, result_rx) = unbounded();
let window = num_workers
.saturating_add(prefetch_depth)
.min(chunks.len())
.max(1);
let epoch = Arc::new(Epoch::new(
Arc::clone(dataset),
Arc::clone(collator),
items_in_parallel,
Arc::clone(pool),
chunks,
window,
num_workers,
result_tx,
));
epoch.spawn_tasks();
Self {
epoch,
result_rx,
reorder: std::iter::repeat_with(|| None).take(window).collect(),
next_out: 0,
pid: std::process::id(),
}
}
fn check_forked(&mut self) -> Result<()> {
if self.pid == std::process::id() {
return Ok(());
}
self.next_out = self.epoch.len();
Err(forked())
}
fn next(&mut self) -> Option<Result<C::Batch>> {
if self.next_out == self.epoch.len() {
return None;
}
let window = self.reorder.len();
loop {
if let Some(batch) = self.reorder[self.next_out % window].take() {
self.next_out += 1;
self.epoch.advance(self.next_out);
return Some(batch);
}
let msg = match self.result_rx.try_recv() {
Ok(msg) => msg,
Err(TryRecvError::Empty) => {
if let Err(e) = self.check_forked() {
return Some(Err(e));
}
match self.result_rx.recv() {
Ok(msg) => msg,
Err(_) => return None,
}
}
Err(TryRecvError::Disconnected) => return None,
};
self.store(msg);
}
}
#[cfg(feature = "python")]
fn next_until(&mut self, deadline: Option<Instant>) -> Option<Option<Result<C::Batch>>> {
if self.next_out == self.epoch.len() {
return Some(None);
}
let window = self.reorder.len();
while self.reorder[self.next_out % window].is_none() {
let msg = match deadline {
None => self.result_rx.try_recv().ok()?,
Some(deadline) => match self.check_forked() {
Err(e) => return Some(Some(Err(e))),
Ok(()) => match self.result_rx.recv_deadline(deadline) {
Ok(msg) => msg,
Err(RecvTimeoutError::Timeout) => return None,
Err(RecvTimeoutError::Disconnected) => return Some(None),
},
},
};
self.store(msg);
}
Some(self.next())
}
fn store(&mut self, (idx, result): WorkerMsg<C::Batch>) {
match result {
Ok(batch) => {
let window = self.reorder.len();
self.reorder[idx % window] = Some(batch);
}
Err(payload) => {
self.next_out = self.epoch.len();
std::panic::resume_unwind(payload);
}
}
}
fn len(&self) -> usize {
self.epoch.len() - self.next_out
}
}
impl<D, C> Drop for ParallelCore<D, C>
where
D: Dataset,
C: Collator<D::Item>,
{
fn drop(&mut self) {
self.epoch.cancel();
}
}
enum Inner<D, C>
where
D: Dataset,
C: Collator<D::Item>,
{
Direct {
chunks: std::vec::IntoIter<Vec<usize>>,
dataset: Arc<D>,
collator: Arc<C>,
items_in_parallel: usize,
pool: Option<Arc<rayon::ThreadPool>>,
pid: u32,
},
Parallel(ParallelCore<D, C>),
}
pub struct DataLoaderIter<D, C>
where
D: Dataset,
C: Collator<D::Item>,
{
inner: Inner<D, C>,
}
impl<D, C> DataLoaderIter<D, C>
where
D: Dataset,
C: Collator<D::Item>,
C::Batch: Send + 'static,
{
pub(super) fn new(
dataset: &Arc<D>,
collator: &Arc<C>,
pool: Option<&Arc<rayon::ThreadPool>>,
items_in_parallel: usize,
num_workers: usize,
prefetch_depth: usize,
chunks: Vec<Vec<usize>>,
) -> Self {
let inner = match pool {
Some(pool) if num_workers > 0 => Inner::Parallel(ParallelCore::new(
dataset,
collator,
pool,
items_in_parallel,
num_workers,
prefetch_depth,
chunks,
)),
_ => Inner::Direct {
chunks: chunks.into_iter(),
dataset: Arc::clone(dataset),
collator: Arc::clone(collator),
items_in_parallel,
pool: pool.cloned(),
pid: std::process::id(),
},
};
Self { inner }
}
#[cfg(feature = "python")]
pub(crate) fn next_until(
&mut self,
deadline: Option<Instant>,
) -> Option<Option<Result<C::Batch>>> {
if let Inner::Parallel(core) = &mut self.inner {
return core.next_until(deadline);
}
deadline.map(|_| self.next())
}
}
impl<D, C> Iterator for DataLoaderIter<D, C>
where
D: Dataset,
C: Collator<D::Item>,
C::Batch: Send + 'static,
{
type Item = Result<C::Batch>;
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
Inner::Direct {
chunks,
dataset,
collator,
items_in_parallel,
pool,
pid,
} => {
let indices = chunks.next()?;
if pool.is_some() && *pid != std::process::id() {
*chunks = Vec::new().into_iter();
return Some(Err(forked()));
}
Some(process_batch(
&**dataset,
&indices,
&**collator,
*items_in_parallel,
pool.as_deref(),
))
}
Inner::Parallel(core) => core.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let n = match &self.inner {
Inner::Direct { chunks, .. } => chunks.len(),
Inner::Parallel(core) => core.len(),
};
(n, Some(n))
}
}
impl<D, C> ExactSizeIterator for DataLoaderIter<D, C>
where
D: Dataset,
C: Collator<D::Item>,
C::Batch: Send + 'static,
{
}