#[cfg(feature = "cuda")]
use crate::api::cuda_batch_scheduler::{
slot_recorder, token_callback, CudaBatchRequest, TokenCallback, TokenSender,
};
#[cfg(feature = "cuda")]
use crate::gguf::OwnedQuantizedModelCuda;
use renacer_core::PhaseTimer;
use std::collections::VecDeque;
use std::sync::Arc;
#[cfg(feature = "cuda")]
pub struct IterationSchedulerConfig {
pub max_slots: usize,
pub prefill_chunk_size: usize,
pub token_budget: usize,
}
#[cfg(feature = "cuda")]
impl IterationSchedulerConfig {
#[must_use]
pub fn report(&self, admission_ceiling_reason: &'static str) -> crate::api::SchedulerReport {
crate::api::SchedulerReport {
kind: "iteration",
max_in_flight: self.max_slots,
window_ms: 0,
prefill_chunk_size: Some(self.prefill_chunk_size),
token_budget: (self.token_budget > 0).then_some(self.token_budget),
slots_admitted: self.max_slots,
admission_ceiling_reason,
in_flight_now: None,
peak_in_flight: None,
}
}
}
#[cfg(feature = "cuda")]
impl Default for IterationSchedulerConfig {
fn default() -> Self {
let max_slots = std::env::var("CUDA_MAX_BATCH")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(4);
let prefill_chunk_size = std::env::var("PREFILL_CHUNK_SIZE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(256);
let token_budget = std::env::var("ITERATION_TOKEN_BUDGET")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0); Self {
max_slots,
prefill_chunk_size,
token_budget,
}
}
}
#[cfg(feature = "cuda")]
struct SchedulerOutput {
num_decode: usize,
prefill_chunk: Option<PrefillChunk>,
}
#[cfg(feature = "cuda")]
struct PrefillChunk {
waiting_idx: usize,
start_token: usize,
end_token: usize,
is_final: bool,
}
#[cfg(feature = "cuda")]
pub fn spawn_iteration_scheduler(
model: Arc<std::sync::RwLock<OwnedQuantizedModelCuda>>,
config: IterationSchedulerConfig,
) -> tokio::sync::mpsc::Sender<CudaBatchRequest> {
let (tx, rx) = tokio::sync::mpsc::channel::<CudaBatchRequest>(256);
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("PMAT-088: failed to create scheduler runtime");
rt.block_on(async move {
iteration_scheduler_loop(model, config, rx).await;
});
});
tx
}
#[cfg(feature = "cuda")]
struct ChunkedPrefillState {
request: CudaBatchRequest,
tokens_prefilled: usize,
total_tokens: usize,
}
#[cfg(feature = "cuda")]
async fn iteration_scheduler_loop(
model: Arc<std::sync::RwLock<OwnedQuantizedModelCuda>>,
config: IterationSchedulerConfig,
mut rx: tokio::sync::mpsc::Receiver<CudaBatchRequest>,
) {
eprintln!(
"[PMAT-088] Iteration scheduler started: max_slots={}, prefill_chunk={}, token_budget={}",
config.max_slots,
config.prefill_chunk_size,
if config.token_budget == 0 {
"unlimited".to_string()
} else {
config.token_budget.to_string()
}
);
let mut waiting: VecDeque<CudaBatchRequest> = VecDeque::new();
let mut batch_active = false;
let mut total_batches: u64 = 0;
let mut total_iterations: u64 = 0;
loop {
if !batch_active && waiting.is_empty() {
match rx.recv().await {
Some(req) => waiting.push_back(req),
None => {
eprintln!("[PMAT-088] Iteration scheduler shutting down");
return;
},
}
}
tokio::task::yield_now().await;
while let Ok(req) = rx.try_recv() {
waiting.push_back(req);
}
if waiting.is_empty() && !batch_active {
continue;
}
if !batch_active {
let batch: Vec<CudaBatchRequest> = vec![waiting.pop_front().unwrap()];
total_batches += 1;
let batch_start = std::time::Instant::now();
eprintln!(
"[PMAT-088c] Batch #{}: starting m=1 (decode-maximal), waiting={} for mid-batch join",
total_batches,
waiting.len(),
);
process_iteration_batch(
&model,
batch,
&mut rx,
&mut waiting,
config.max_slots,
&mut total_iterations,
);
let elapsed = batch_start.elapsed();
eprintln!(
"[PMAT-088] Batch #{} done in {:.1}ms, {} iterations",
total_batches,
elapsed.as_secs_f64() * 1000.0,
total_iterations,
);
}
}
}
#[cfg(feature = "cuda")]
struct IterationBatch {
state: crate::gguf::BatchedDecodeState,
error_senders: Vec<Option<TokenSender>>,
phase_timer: PhaseTimer,
}
#[cfg(feature = "cuda")]
pub(crate) fn process_iteration_batch(
model: &Arc<std::sync::RwLock<OwnedQuantizedModelCuda>>,
batch: Vec<CudaBatchRequest>,
rx: &mut tokio::sync::mpsc::Receiver<CudaBatchRequest>,
waiting: &mut VecDeque<CudaBatchRequest>,
max_slots: usize,
total_iterations: &mut u64,
) {
if crate::api::batch_admission::fast_path_eligible(
batch.len(),
waiting.is_empty(),
crate::api::batch_admission::force_batched_path(),
) {
let req = batch.into_iter().next().unwrap();
let mut cuda_model = model.write().expect("PMAT-088: model lock poisoned");
crate::api::cuda_batch_scheduler::generate_single_request(&mut cuda_model, req);
*total_iterations += 1;
return;
}
let Some(mut running) = iteration_setup(model, batch, max_slots) else {
return;
};
while !iteration_batch_complete(&running.state, rx, waiting) {
*total_iterations += 1;
if !iteration_step(model, &mut running, rx, waiting, *total_iterations) {
return;
}
}
{
let mut cuda_model = model.write().expect("PMAT-088: model lock poisoned");
cuda_model.batched_cleanup(&running.state);
}
}
#[cfg(feature = "cuda")]
fn iteration_setup(
model: &Arc<std::sync::RwLock<OwnedQuantizedModelCuda>>,
mut batch: Vec<CudaBatchRequest>,
max_slots: usize,
) -> Option<IterationBatch> {
use crate::gguf::QuantizedGenerateConfig;
let m = batch.len();
let prompts: Vec<Vec<u32>> = batch.iter().map(|r| r.prompt_ids.clone()).collect();
let configs: Vec<QuantizedGenerateConfig> = batch.iter().map(|r| r.config.clone()).collect();
let error_senders: Vec<Option<TokenSender>> =
batch.iter().map(|r| Some(r.token_tx.clone())).collect();
let recorders: Vec<Option<crate::gguf::SlotRecorder>> = batch
.iter_mut()
.map(|r| r.logprobs.take().map(slot_recorder))
.collect();
let callbacks: Vec<TokenCallback> = batch
.into_iter()
.map(|req| token_callback(req.token_tx))
.collect();
let setup = {
let mut cuda_model = model.write().expect("PMAT-088: model lock poisoned");
cuda_model.batched_setup_and_prefill(&prompts, &configs, callbacks, max_slots)
};
match setup {
Ok(mut state) => {
state.recorders = recorders;
Some(IterationBatch {
state,
error_senders,
phase_timer: PhaseTimer::from_env("PMAT_283_TIMING", "PMAT-283"),
})
},
Err(e) => {
eprintln!("[PMAT-088] Setup+prefill ERROR (m={m}): {e}");
for tx in error_senders.iter().flatten() {
let _ = tx.try_send(Err(e.to_string()));
}
None
},
}
}
#[cfg(feature = "cuda")]
fn iteration_batch_complete(
state: &crate::gguf::BatchedDecodeState,
rx: &mut tokio::sync::mpsc::Receiver<CudaBatchRequest>,
waiting: &mut VecDeque<CudaBatchRequest>,
) -> bool {
if !state.all_done() {
return false;
}
while let Ok(req) = rx.try_recv() {
waiting.push_back(req);
}
waiting.is_empty() || state.gen_idx >= state.max_tokens_max
}
#[cfg(feature = "cuda")]
fn iteration_step(
model: &Arc<std::sync::RwLock<OwnedQuantizedModelCuda>>,
running: &mut IterationBatch,
rx: &mut tokio::sync::mpsc::Receiver<CudaBatchRequest>,
waiting: &mut VecDeque<CudaBatchRequest>,
iteration: u64,
) -> bool {
let iter_start = std::time::Instant::now();
running.phase_timer.start();
let token_ids = {
let mut cuda_model = model.write().expect("PMAT-088: model lock poisoned");
running.phase_timer.mark("lock");
schedule_waiting(&mut cuda_model, running, rx, waiting);
running.phase_timer.mark("sched");
match cuda_model.batched_decode_step(&mut running.state) {
Ok(ids) => ids,
Err(e) => {
eprintln!(
"[PMAT-088] Decode step ERROR (m={}, step={}): {e}",
running.state.m, running.state.gen_idx
);
for tx in running.error_senders.iter().flatten() {
let _ = tx.try_send(Err(e.to_string()));
}
cuda_model.batched_cleanup(&running.state);
return false;
},
}
};
running.phase_timer.mark("decode");
running.state.distribute_tokens(&token_ids);
running.phase_timer.mark("dist");
close_done_slots(running);
running.phase_timer.emit(iteration, running.state.m);
if iteration <= 3 || running.state.gen_idx % 50 == 0 {
let iter_ms = iter_start.elapsed().as_secs_f64() * 1000.0;
let active_slots = running.state.done.iter().filter(|&&d| !d).count();
eprintln!(
"[PMAT-088] iter={}, m={}, active={}, step_ms={:.1}",
iteration, running.state.m, active_slots, iter_ms,
);
}
true
}
#[cfg(feature = "cuda")]
fn schedule_waiting(
cuda_model: &mut OwnedQuantizedModelCuda,
running: &mut IterationBatch,
rx: &mut tokio::sync::mpsc::Receiver<CudaBatchRequest>,
waiting: &mut VecDeque<CudaBatchRequest>,
) {
if running.state.done.iter().any(|&d| d) {
recycle_done_slots(cuda_model, running, rx, waiting);
} else if running.state.m < running.state.max_kv_slots {
join_waiting_slot(cuda_model, running, rx, waiting);
}
}
#[cfg(feature = "cuda")]
fn recycle_done_slots(
cuda_model: &mut OwnedQuantizedModelCuda,
running: &mut IterationBatch,
rx: &mut tokio::sync::mpsc::Receiver<CudaBatchRequest>,
waiting: &mut VecDeque<CudaBatchRequest>,
) {
let mut recycle_pairs: Vec<(
usize,
Vec<u32>,
crate::gguf::QuantizedGenerateConfig,
TokenCallback,
)> = Vec::new();
let mut recycle_error_txs: Vec<(usize, TokenSender, Option<crate::gguf::SlotRecorder>)> =
Vec::new();
for slot_idx in 0..running.state.m {
if !running.state.done[slot_idx] {
continue;
}
let Some(req) = waiting.pop_front().or_else(|| rx.try_recv().ok()) else {
break; };
recycle_error_txs.push((
slot_idx,
req.token_tx.clone(),
req.logprobs.map(slot_recorder),
));
recycle_pairs.push((
slot_idx,
req.prompt_ids,
req.config,
token_callback(req.token_tx),
));
}
if recycle_pairs.is_empty() {
return;
}
match cuda_model.recycle_slots_batch(&mut running.state, recycle_pairs) {
Ok(()) => {
for (slot_idx, error_tx, recorder) in recycle_error_txs {
running.error_senders[slot_idx] = Some(error_tx);
running.state.recorders[slot_idx] = recorder;
}
},
Err(e) => {
eprintln!("[PMAT-088d] Batch recycle FAILED: {e}");
for (_, error_tx, _) in &recycle_error_txs {
let _ = error_tx.try_send(Err(e.to_string()));
}
},
}
}
#[cfg(feature = "cuda")]
fn join_waiting_slot(
cuda_model: &mut OwnedQuantizedModelCuda,
running: &mut IterationBatch,
rx: &mut tokio::sync::mpsc::Receiver<CudaBatchRequest>,
waiting: &mut VecDeque<CudaBatchRequest>,
) {
let Some(req) = waiting.pop_front().or_else(|| rx.try_recv().ok()) else {
return;
};
let error_tx = req.token_tx.clone();
let recorder = req.logprobs.map(slot_recorder);
match cuda_model.add_slot_to_batch(
&mut running.state,
req.prompt_ids,
req.config,
token_callback(req.token_tx),
) {
Ok(()) => {
running.state.recorders[running.state.m - 1] = recorder;
running.error_senders.push(Some(error_tx));
},
Err(e) => {
eprintln!("[PMAT-088c] Mid-batch join FAILED: {e}");
let _ = error_tx.try_send(Err(e.to_string()));
},
}
}
#[cfg(feature = "cuda")]
fn close_done_slots(running: &mut IterationBatch) {
let IterationBatch {
state,
error_senders,
..
} = running;
for slot_idx in 0..state.m {
if state.done[slot_idx]
&& error_senders
.get(slot_idx)
.and_then(|o| o.as_ref())
.is_some()
{
state.on_tokens[slot_idx] = Box::new(|_| false);
error_senders[slot_idx] = None;
}
}
}