use std::collections::VecDeque;
use std::sync::mpsc::Receiver;
use numr::dtype::DType;
use numr::runtime::Runtime;
use numr::tensor::Tensor;
use boostr::model::audio::voxcpm::model::{
GenerateOptions, GenerateState, PrefillRow, VoxCpm2Model,
};
use crate::error::{Error, Result};
use crate::voxcpm::engine::batch::{EncodeJob, FREE_ROW, Job, Sampler, Work, clamp_max_batch};
use crate::voxcpm::engine::continuous::refuse::{refuse_queue, too_large};
use crate::voxcpm::engine::continuous::slots::{Slots, drain};
use crate::voxcpm::engine::queue::{collect_batch, recv_job};
use crate::voxcpm::engine::run::Row;
use crate::voxcpm::engine::types::{EngineInner, MAX_LEN_CAP, VoxCpm2RenderClient};
pub(in crate::voxcpm::engine) fn run_continuous<R>(inner: &EngineInner<R>, jobs: &Receiver<Work<R>>)
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
let width = clamp_max_batch(inner.options.max_batch);
let capacity = match slot_capacity(inner) {
Ok(capacity) => capacity,
Err(err) => return refuse_queue(jobs, &err),
};
let mut batch = match Batch::open(inner, width, capacity) {
Ok(batch) => batch,
Err(err) => return refuse_queue(jobs, &err),
};
let mut serve = |encode: EncodeJob<R>| inner.serve_encode(encode);
let mut closed = false;
loop {
if !closed {
closed = batch.admit_queued(inner, jobs, &mut serve);
}
if !batch.slots.any_occupied() {
if let Some(first) = batch.held.pop_front() {
batch.cold_start(inner, jobs, first, &mut serve);
continue;
}
if closed {
return;
}
match recv_job(jobs, &mut serve) {
Some(first) => batch.cold_start(inner, jobs, first, &mut serve),
None => return,
}
continue;
}
if let Err(err) = batch.step(inner) {
batch.fail_rows(&err);
drop(batch);
return refuse_queue(jobs, &err);
}
}
}
fn slot_capacity<R: Runtime<DType = DType>>(inner: &EngineInner<R>) -> Result<usize> {
let want = inner.options.continuous_capacity;
Ok(match inner.model.lane_rope_positions()? {
Some(positions) => want.min(positions),
None => want,
})
}
struct Batch<R: Runtime<DType = DType>> {
state: GenerateState<R>,
slots: Slots<Row<R>>,
options: GenerateOptions,
capacity: usize,
sampler: Option<Sampler>,
held: VecDeque<Job<R>>,
}
impl<R> Batch<R>
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
fn open(inner: &EngineInner<R>, width: usize, capacity: usize) -> Result<Self> {
let mut options = GenerateOptions::new(MAX_LEN_CAP, inner.options.seed);
options.min_len = inner.options.min_len;
let mut batch = Self {
state: GenerateState::open(&inner.model, width, capacity)?,
slots: Slots::new(width),
options,
capacity,
sampler: None,
held: VecDeque::new(),
};
batch.refresh_options();
Ok(batch)
}
fn admit_queued(
&mut self,
inner: &EngineInner<R>,
jobs: &Receiver<Work<R>>,
serve: &mut dyn FnMut(EncodeJob<R>),
) -> bool {
let (taken, closed) = drain(jobs, self.slots.free_count(), serve);
for job in taken {
if self.held.is_empty() && self.runs(&job) {
self.seat_next(inner, job);
} else {
self.held.push_back(job);
}
}
closed
}
fn runs(&self, job: &Job<R>) -> bool {
self.sampler.is_none_or(|sampler| sampler == job.sampler)
}
fn cold_start(
&mut self,
inner: &EngineInner<R>,
jobs: &Receiver<Work<R>>,
first: Job<R>,
serve: &mut dyn FnMut(EncodeJob<R>),
) {
let free = self.slots.free_count();
let window = inner.options.batch_window;
for job in collect_batch(jobs, first, window, free, serve, &mut self.held) {
self.seat_next(inner, job);
}
}
fn seat_next(&mut self, inner: &EngineInner<R>, job: Job<R>) {
if !self.runs(&job) {
self.held.push_back(job);
return;
}
match self.slots.free() {
Some(slot) => self.seat(inner, slot, job),
None => job.reply.fail(Error::ModelError {
reason: format!(
"VoxCPM2 continuous batch took a request with no free slot among its {}; \
this is an engine bug, report it with the batch width",
self.slots.width()
),
}),
}
}
fn seat(&mut self, inner: &EngineInner<R>, slot: usize, job: Job<R>) {
if job.seq_len + job.max_len >= self.capacity {
job.reply.fail(too_large(&job, self.capacity));
return;
}
let ref_feat = match inner.reference(&job.voice) {
Ok(ref_feat) => ref_feat,
Err(err) => return job.reply.fail(err),
};
let client = inner.client.as_ref();
let prefilled = match inner.model.prefill_batch(
client,
&[PrefillRow {
ref_feat,
text_token_ids: &job.text_token_ids,
prompt_feat: job.prompt.as_deref(),
}],
job.seq_len,
) {
Ok(prefilled) => prefilled,
Err(err) => return job.reply.fail(Error::from(err)),
};
if let Err(err) = self
.state
.admit(client, slot, &prefilled, job.row_options())
{
return job.reply.fail(Error::from(err));
}
if self.sampler.is_none() {
job.sampler.apply(&mut self.options);
self.sampler = Some(job.sampler);
}
self.slots.seat(
slot,
Row {
job,
live: true,
emitted: 0,
},
);
self.refresh_options();
}
fn step(&mut self, inner: &EngineInner<R>) -> Result<()> {
let client = inner.client.as_ref();
let chunk = inner.options.stream_chunk_patches.max(1);
inner
.model
.patch_generator()
.step(client, &mut self.state, &self.options)?;
let mut released = false;
for slot in 0..self.slots.width() {
let retire = {
let Some(row) = self.slots.get_mut(slot) else {
continue;
};
inner.pump_row(&mut self.state, slot, row, chunk);
let done = !row.live || self.state.finished(slot);
let again = done && row.retries(self.state.patch_len[slot]);
if done && !again {
inner.deliver_row(&self.state, slot, row);
}
(done, again)
};
if let (true, again) = retire {
let row = self.slots.take(slot);
self.state.release(client, slot)?;
released = true;
if let Some(row) = row.filter(|_| again) {
self.seat(inner, slot, row.job.retried());
}
}
}
if released {
self.refresh_options();
if !self.slots.any_occupied() {
self.sampler = None;
}
}
Ok(())
}
fn fail_rows(&mut self, err: &Error) {
for (_, row) in self.slots.drain_all() {
if row.live {
row.job.reply.fail(Error::ModelError {
reason: err.to_string(),
});
}
}
for job in self.held.drain(..) {
job.reply.fail(Error::ModelError {
reason: err.to_string(),
});
}
}
fn refresh_options(&mut self) {
self.options.rows = self
.state
.slot_options
.iter()
.map(|seated| seated.unwrap_or(FREE_ROW))
.collect();
}
}