use std::collections::VecDeque;
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::time::{Duration, Instant};
use numr::runtime::Runtime;
use crate::voxcpm::engine::batch::{EncodeJob, Job, Work};
fn take_held<R: Runtime>(batch: &mut Vec<Job<R>>, held: &mut VecDeque<Job<R>>, max_batch: usize) {
let Some(sampler) = batch.first().map(|job| job.sampler) else {
return;
};
let mut at = 0;
while at < held.len() && batch.len() < max_batch {
if held[at].sampler == sampler {
if let Some(job) = held.remove(at) {
batch.push(job);
}
} else {
at += 1;
}
}
}
pub(super) fn route<R: Runtime>(job: Job<R>, batch: &mut Vec<Job<R>>, held: &mut VecDeque<Job<R>>) {
let joins = held.is_empty()
&& batch
.first()
.is_none_or(|first| first.sampler == job.sampler);
if joins {
batch.push(job);
} else {
held.push_back(job);
}
}
pub(super) fn recv_job<R: Runtime>(
jobs: &Receiver<Work<R>>,
serve: &mut dyn FnMut(EncodeJob<R>),
) -> Option<Job<R>> {
loop {
match jobs.recv() {
Ok(Work::Render(job)) => return Some(job),
Ok(Work::Encode(encode)) => serve(encode),
Err(_) => return None,
}
}
}
pub(super) fn collect_batch<R: Runtime>(
jobs: &Receiver<Work<R>>,
first: Job<R>,
window: Duration,
max_batch: usize,
serve: &mut dyn FnMut(EncodeJob<R>),
held: &mut VecDeque<Job<R>>,
) -> Vec<Job<R>> {
let mut batch = Vec::with_capacity(max_batch.max(1));
batch.push(first);
take_held(&mut batch, held, max_batch);
let deadline = Instant::now() + window;
while batch.len() < max_batch {
let now = Instant::now();
if now >= deadline {
break;
}
match jobs.recv_timeout(deadline - now) {
Ok(Work::Render(job)) => route(job, &mut batch, held),
Ok(Work::Encode(encode)) => serve(encode),
Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => break,
}
}
batch
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Result;
use crate::voxcpm::engine::batch::{JobVoice, test_sampler};
use crate::voxcpm::engine::reply::Reply;
use crate::voxcpm::engine::request::Badcase;
use boostr::model::audio::voxcpm::model::PadSide;
use numr::runtime::cpu::CpuRuntime;
use numr::tensor::Tensor;
use std::sync::mpsc::channel;
type TestJob = Job<CpuRuntime>;
type TestWork = Work<CpuRuntime>;
fn render() -> TestWork {
Work::Render(job_at(0, 10))
}
fn job_at(seed: u64, steps: usize) -> TestJob {
let (tx, _rx) = channel();
TestJob {
voice: JobVoice::ZeroShot,
prompt: None,
seed,
sampler: test_sampler(steps),
text_token_ids: vec![1, 2],
max_len: 22,
seq_len: 2,
target_len: 2,
badcase: Badcase::default(),
attempt: 1,
reply: Reply::Buffered(tx),
}
}
fn encode() -> (TestWork, Receiver<Result<Tensor<CpuRuntime>>>) {
let (tx, rx) = channel();
(
Work::Encode(EncodeJob {
samples: vec![0.0; 4],
side: PadSide::Right,
reply: tx,
}),
rx,
)
}
fn held() -> VecDeque<TestJob> {
VecDeque::new()
}
fn seeds(batch: &[TestJob]) -> Vec<u64> {
batch.iter().map(|job| job.seed).collect()
}
#[test]
fn a_job_with_another_sampler_waits_for_its_own_batch() {
let (tx, rx) = channel();
tx.send(Work::Render(job_at(1, 10))).expect("queue");
tx.send(Work::Render(job_at(2, 16))).expect("queue");
tx.send(Work::Render(job_at(3, 10))).expect("queue");
tx.send(Work::Render(job_at(4, 16))).expect("queue");
drop(tx);
let mut served = 0;
let mut held = held();
let first = first_job(&rx, &mut served);
let window = Duration::from_secs(30);
let batch = collect_batch(&rx, first, window, 8, &mut |_| {}, &mut held);
assert_eq!(seeds(&batch), [1]);
assert_eq!(
held.iter().map(|job| job.seed).collect::<Vec<_>>(),
[2, 3, 4]
);
let first = held.pop_front().expect("a held job");
let batch = collect_batch(&rx, first, window, 8, &mut |_| {}, &mut held);
assert_eq!(seeds(&batch), [2, 4]);
assert_eq!(held.iter().map(|job| job.seed).collect::<Vec<_>>(), [3]);
}
#[test]
fn held_jobs_join_up_to_the_cap() {
let (tx, rx) = channel::<TestWork>();
drop(tx);
let mut held: VecDeque<TestJob> =
[job_at(2, 10), job_at(3, 16), job_at(4, 10), job_at(5, 10)].into();
let batch = collect_batch(
&rx,
job_at(1, 10),
Duration::ZERO,
3,
&mut |_| {},
&mut held,
);
assert_eq!(seeds(&batch), [1, 2, 4]);
assert_eq!(held.iter().map(|job| job.seed).collect::<Vec<_>>(), [3, 5]);
}
fn first_job(rx: &Receiver<TestWork>, served: &mut usize) -> TestJob {
recv_job(rx, &mut |_| *served += 1).expect("a render is queued")
}
#[test]
fn encodes_ahead_of_a_render_are_served_first() {
let (tx, rx) = channel();
let (first, _first_rx) = encode();
let (second, _second_rx) = encode();
tx.send(first).expect("queue");
tx.send(second).expect("queue");
tx.send(render()).expect("queue");
let mut served = 0;
first_job(&rx, &mut served);
assert_eq!(served, 2);
}
#[test]
fn a_closed_queue_serves_what_is_left_and_ends() {
let (tx, rx) = channel();
let (only, _only_rx) = encode();
tx.send(only).expect("queue");
drop(tx);
let mut served = 0;
assert!(recv_job(&rx, &mut |_| served += 1).is_none());
assert_eq!(served, 1);
}
#[test]
fn queued_jobs_batch_together() {
let (tx, rx) = channel();
for _ in 0..3 {
tx.send(render()).expect("queue");
}
let (between, _between_rx) = encode();
tx.send(between).expect("queue");
let mut served = 0;
let first = first_job(&rx, &mut served);
let window = Duration::from_millis(50);
let started = Instant::now();
let batch = collect_batch(&rx, first, window, 8, &mut |_| served += 1, &mut held());
assert_eq!(batch.len(), 3);
assert_eq!(served, 1, "the encode was served, not batched");
assert!(started.elapsed() >= window, "ended the window early");
}
#[test]
fn a_full_batch_starts_without_waiting() {
let (tx, rx) = channel();
for _ in 0..4 {
tx.send(render()).expect("queue");
}
let mut served = 0;
let first = first_job(&rx, &mut served);
let started = Instant::now();
let batch = collect_batch(
&rx,
first,
Duration::from_secs(30),
2,
&mut |_| {},
&mut held(),
);
assert_eq!(batch.len(), 2, "the cap holds");
assert!(
started.elapsed() < Duration::from_secs(5),
"waited the window out"
);
}
#[test]
fn a_lone_job_on_a_closed_queue_starts_at_once() {
let (tx, rx) = channel();
tx.send(render()).expect("queue");
drop(tx);
let mut served = 0;
let first = first_job(&rx, &mut served);
let started = Instant::now();
let batch = collect_batch(
&rx,
first,
Duration::from_secs(30),
8,
&mut |_| {},
&mut held(),
);
assert_eq!(batch.len(), 1);
assert!(
started.elapsed() < Duration::from_secs(5),
"waited the window out"
);
}
#[test]
fn a_cap_of_one_never_waits() {
let (tx, rx) = channel();
for _ in 0..2 {
tx.send(render()).expect("queue");
}
let mut served = 0;
let first = first_job(&rx, &mut served);
let started = Instant::now();
let batch = collect_batch(
&rx,
first,
Duration::from_secs(30),
1,
&mut |_| {},
&mut held(),
);
assert_eq!(batch.len(), 1);
assert!(
started.elapsed() < Duration::from_secs(5),
"waited with batching off"
);
}
#[test]
fn a_lone_job_costs_the_window_and_no_more() {
let (tx, rx) = channel();
tx.send(render()).expect("queue");
let mut served = 0;
let first = first_job(&rx, &mut served);
let window = Duration::from_millis(20);
let started = Instant::now();
let batch = collect_batch(&rx, first, window, 8, &mut |_| {}, &mut held());
let waited = started.elapsed();
assert_eq!(batch.len(), 1);
assert!(waited >= window, "returned before the window: {waited:?}");
assert!(
waited < Duration::from_secs(5),
"waited far past the window"
);
}
}