use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread::JoinHandle;
use numr::dtype::DType;
use numr::runtime::Runtime;
use numr::tensor::Tensor;
use boostr::model::audio::voxcpm::model::{GenerateOptions, PadSide, RowOptions, VoxCpm2Model};
use crate::error::Result;
use crate::voxcpm::engine::queue::{collect_batch, recv_job};
use crate::voxcpm::engine::reply::Reply;
use crate::voxcpm::engine::request::Badcase;
use crate::voxcpm::engine::types::{EngineInner, MAX_LEN_CAP, VoxCpm2RenderClient};
pub(super) use boostr::model::audio::voxcpm::model::{batch_width, clamp_max_batch};
pub(super) fn is_badcase(patches: usize, tokens: usize, ratio: f64) -> bool {
patches as f64 >= tokens as f64 * ratio
}
pub(super) const FREE_ROW: RowOptions = RowOptions {
max_len: 1,
seed: 0,
};
#[derive(Debug, Clone, Copy)]
pub(super) struct Sampler {
pub(super) n_timesteps: usize,
pub(super) cfg_value: f32,
}
impl Sampler {
pub(super) fn apply(self, options: &mut GenerateOptions) {
options.cfm.n_timesteps = self.n_timesteps;
options.cfm.cfg_value = self.cfg_value;
}
}
impl PartialEq for Sampler {
fn eq(&self, other: &Self) -> bool {
self.n_timesteps == other.n_timesteps
&& self.cfg_value.to_bits() == other.cfg_value.to_bits()
}
}
impl Eq for Sampler {}
pub(super) enum JobVoice<R: Runtime> {
Named(String),
Reference(Arc<Tensor<R>>),
ZeroShot,
}
pub(super) struct Job<R: Runtime> {
pub(super) voice: JobVoice<R>,
pub(super) prompt: Option<Arc<Tensor<R>>>,
pub(super) seed: u64,
pub(super) sampler: Sampler,
pub(super) text_token_ids: Vec<u32>,
pub(super) max_len: usize,
pub(super) seq_len: usize,
pub(super) target_len: usize,
pub(super) badcase: Badcase,
pub(super) attempt: usize,
pub(super) reply: Reply,
}
impl<R: Runtime> Job<R> {
pub(super) fn retries(&self, patches: usize) -> bool {
self.reply.stream().is_none()
&& self.attempt < self.badcase.max_attempts
&& is_badcase(patches, self.target_len, self.badcase.ratio)
}
pub(super) fn retried(self) -> Self {
Self {
seed: self.seed.wrapping_add(MAX_LEN_CAP as u64),
attempt: self.attempt + 1,
..self
}
}
pub(super) fn row_options(&self) -> RowOptions {
RowOptions {
max_len: self.max_len,
seed: self.seed,
}
}
}
pub(super) struct EncodeJob<R: Runtime> {
pub(super) samples: Vec<f32>,
pub(super) side: PadSide,
pub(super) reply: Sender<Result<Tensor<R>>>,
}
pub(super) enum Work<R: Runtime> {
Render(Job<R>),
Encode(EncodeJob<R>),
}
pub(super) fn row_options(real: &[RowOptions], width: usize) -> Vec<RowOptions> {
(0..width)
.map(|b| real.get(b).copied().unwrap_or(FREE_ROW))
.collect()
}
pub(super) fn spawn_worker<R>(
inner: Arc<EngineInner<R>>,
) -> Result<(Sender<Work<R>>, JoinHandle<()>)>
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
let (sender, receiver) = channel::<Work<R>>();
let handle = std::thread::Builder::new()
.name("voxcpm2-render".to_string())
.spawn(move || worker_loop(&inner, &receiver))?;
Ok((sender, handle))
}
fn worker_loop<R>(inner: &Arc<EngineInner<R>>, jobs: &Receiver<Work<R>>)
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
if inner.options.continuous_batching {
super::continuous::run_continuous(inner, jobs);
return;
}
let window = inner.options.batch_window;
let max_batch = clamp_max_batch(inner.options.max_batch);
let mut serve = |encode: EncodeJob<R>| inner.serve_encode(encode);
let mut held = VecDeque::new();
loop {
let first = match held.pop_front() {
Some(job) => job,
None => match recv_job(jobs, &mut serve) {
Some(job) => job,
None => return,
},
};
let batch = collect_batch(jobs, first, window, max_batch, &mut serve, &mut held);
for job in inner.run_batch(batch).into_iter().rev() {
held.push_front(job);
}
}
}
#[cfg(test)]
pub(super) fn test_sampler(n_timesteps: usize) -> Sampler {
Sampler {
n_timesteps,
cfg_value: 2.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use numr::runtime::cpu::CpuRuntime;
type TestJob = Job<CpuRuntime>;
#[test]
fn the_default_options_take_the_persistent_batch_route() {
use crate::voxcpm::options::VoxCpm2SynthOptions;
assert!(VoxCpm2SynthOptions::default().continuous_batching);
assert!(
!VoxCpm2SynthOptions {
continuous_batching: false,
..VoxCpm2SynthOptions::default()
}
.continuous_batching
);
}
#[test]
fn the_engine_cap_is_a_width_the_loop_can_run() {
for cap in [0usize, 1, 3, 7, 8, 64] {
let clamped = clamp_max_batch(cap);
assert_eq!(batch_width(clamped), clamped, "cap {cap}");
}
}
#[test]
fn row_options_carry_each_rows_own_seed_and_pad_free_slots() {
let real = [
RowOptions {
max_len: 30,
seed: 7,
},
RowOptions {
max_len: 12,
seed: 1007,
},
];
let opts = row_options(&real, 4);
assert_eq!(opts, [real[0], real[1], FREE_ROW, FREE_ROW]);
assert_eq!(FREE_ROW.max_len, 1);
}
#[test]
fn row_options_keep_job_order() {
let real: Vec<RowOptions> = [(5, 3), (6, 1), (7, 2)]
.into_iter()
.map(|(max_len, seed)| RowOptions { max_len, seed })
.collect();
let opts = row_options(&real, 4);
let caps: Vec<usize> = opts.iter().map(|r| r.max_len).collect();
let seeds: Vec<u64> = opts.iter().take(3).map(|r| r.seed).collect();
assert_eq!(caps, [5, 6, 7, 1]);
assert_eq!(seeds, [3, 1, 2]);
}
#[test]
fn a_take_is_bad_once_it_reaches_the_ratio() {
assert!(!is_badcase(11, 2, 6.0));
assert!(is_badcase(12, 2, 6.0));
assert!(is_badcase(22, 2, 6.0));
assert!(is_badcase(1, 0, 6.0));
}
#[test]
fn a_bad_take_retries_with_fresh_noise_until_the_last_attempt() {
let (tx, _rx) = channel();
let job = TestJob {
voice: JobVoice::ZeroShot,
prompt: None,
seed: 7,
sampler: test_sampler(10),
text_token_ids: vec![1, 2],
max_len: 22,
seq_len: 2,
target_len: 2,
badcase: Badcase::default(),
attempt: 1,
reply: Reply::Buffered(tx),
};
assert!(!job.retries(11), "a good take retried");
assert!(job.retries(22));
let second = job.retried();
assert_eq!((second.attempt, second.seed), (2, 7 + MAX_LEN_CAP as u64));
assert!(second.retries(22));
let third = second.retried();
assert_eq!(third.seed, 7 + 2 * MAX_LEN_CAP as u64);
assert!(!third.retries(22), "the last attempt retried");
let off = TestJob {
badcase: Badcase::no_retry(6.0),
attempt: 1,
..third
};
assert!(!off.retries(22));
let (chunks, _rx) = channel();
let streamed = TestJob {
badcase: Badcase::default(),
reply: Reply::Streaming {
chunks,
cancelled: Arc::default(),
},
..off
};
assert!(!streamed.retries(22), "a streamed take retried");
}
#[test]
fn a_job_hands_the_loop_its_own_cap_and_seed() {
let (tx, _rx) = channel();
let job = TestJob {
voice: JobVoice::ZeroShot,
prompt: None,
seed: 42,
sampler: test_sampler(10),
text_token_ids: vec![1, 2],
max_len: 22,
seq_len: 2,
target_len: 2,
badcase: Badcase::default(),
attempt: 1,
reply: Reply::Buffered(tx),
};
assert_eq!(
job.row_options(),
RowOptions {
max_len: 22,
seed: 42
}
);
}
}