use crate::frontend::generation::StageOpenAiBackend;
use openai_frontend::{OpenAiError, OpenAiResult};
use skippy_runtime::{IterationBatchPhase, SamplingConfig};
use std::time::Instant;
const MAX_NATIVE_ITERATION_TOKENS: usize = 2_048;
fn suffix_chunk_sample_flags(
suffix_len: usize,
chunk_tokens: usize,
sample_last: bool,
) -> Vec<bool> {
let chunk_count = suffix_len.div_ceil(chunk_tokens);
(0..chunk_count)
.map(|index| sample_last && index == chunk_count.saturating_sub(1))
.collect()
}
pub(in crate::frontend) struct SuffixPrefillOutcome {
pub(in crate::frontend) predicted: Option<i32>,
pub(in crate::frontend) chunk_count: usize,
pub(in crate::frontend) max_batch_size: usize,
pub(in crate::frontend) runtime_lock_wait_ms: f64,
pub(in crate::frontend) runtime_lock_hold_ms: f64,
}
impl StageOpenAiBackend {
pub(super) fn prefill_suffix(
&self,
session_id: &str,
suffix: &[i32],
sampling: Option<&SamplingConfig>,
sample_last: bool,
deadline: Instant,
cancellation: Option<&openai_frontend::CancellationToken>,
) -> OpenAiResult<SuffixPrefillOutcome> {
if suffix.is_empty() {
return Err(OpenAiError::backend(
"deferred suffix prefill requires at least one token",
));
}
let chunk_tokens = usize::try_from(self.config.n_ubatch.unwrap_or(256))
.unwrap_or(MAX_NATIVE_ITERATION_TOKENS)
.clamp(1, MAX_NATIVE_ITERATION_TOKENS);
let sample_flags = suffix_chunk_sample_flags(suffix.len(), chunk_tokens, sample_last);
let channel = self.iteration_scheduler.direct_iteration_channel();
let mut predicted = None;
let mut chunk_count = 0usize;
let mut max_batch_size = 0usize;
let mut runtime_lock_wait_ms = 0.0;
let mut runtime_lock_hold_ms = 0.0;
for (chunk, should_sample) in suffix.chunks(chunk_tokens).zip(sample_flags) {
ensure_suffix_prefill_active(deadline, cancellation)?;
let outcome = self.iteration_scheduler.execute_iteration_on(
&channel,
session_id,
chunk,
&[],
sampling,
should_sample,
IterationBatchPhase::Prefill,
Some(deadline),
cancellation,
)?;
chunk_count = chunk_count.saturating_add(1);
max_batch_size = max_batch_size.max(outcome.batch_size);
runtime_lock_wait_ms += outcome.runtime_lock_wait_ms;
runtime_lock_hold_ms += outcome.runtime_lock_hold_ms;
if should_sample {
predicted = Some(outcome.predicted);
}
}
Ok(SuffixPrefillOutcome {
predicted,
chunk_count,
max_batch_size,
runtime_lock_wait_ms,
runtime_lock_hold_ms,
})
}
}
fn ensure_suffix_prefill_active(
deadline: Instant,
cancellation: Option<&openai_frontend::CancellationToken>,
) -> OpenAiResult<()> {
if cancellation.is_some_and(openai_frontend::CancellationToken::is_cancelled) {
return Err(OpenAiError::cancelled(
"request cancelled during deferred suffix prefill",
));
}
if Instant::now() >= deadline {
return Err(OpenAiError::timeout(
"cache operation deadline exceeded during deferred suffix prefill",
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::suffix_chunk_sample_flags;
#[test]
fn deferred_suffix_samples_only_the_final_chunk() {
assert_eq!(
suffix_chunk_sample_flags(7, 3, true),
vec![false, false, true]
);
assert_eq!(
suffix_chunk_sample_flags(7, 3, false),
vec![false, false, false]
);
}
#[test]
fn resident_suffix_keeps_final_chunk_sampling() {
assert_eq!(suffix_chunk_sample_flags(3, 3, true), vec![true]);
assert_eq!(suffix_chunk_sample_flags(4, 3, true), vec![false, true]);
}
}