use std::ptr::NonNull;
pub const MAX_SPECULATIVE_STATE_BYTES: usize = 64 * 1024 * 1024;
pub const MAX_SPECULATIVE_SEQUENCES: u32 = 4_096;
pub const MAX_SPECULATIVE_DRAFT_TOKENS: i32 = 4_096;
pub const MAX_SPECULATIVE_PROMPT_TOKENS: usize = 1_048_576;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum SpeculativeStateError {
#[error("speculative state operation received a null native value")]
Null,
#[error("speculative state sequence id is outside the configured range")]
BadSequence,
#[error("speculative state is available only at a quiescent boundary")]
NotQuiescent,
#[error("speculative state size changed from {expected} to {actual} bytes")]
SizeChanged {
expected: usize,
actual: usize,
},
#[error("the speculative state buffer was too small")]
BufferTooSmall,
#[error("the active speculative implementation did not expose complete state")]
Unavailable,
#[error("speculative state is invalid for this session")]
Invalid,
#[error("speculative state size overflowed")]
Overflow,
#[error("the native speculative-state operation raised an exception")]
Exception,
#[error("speculative state is {size} bytes, exceeding the {maximum}-byte bound")]
Excessive {
size: usize,
maximum: usize,
},
#[error("unknown speculative state status {0}")]
Unknown(u32),
}
pub(crate) fn capture_state(
raw: NonNull<llama_cpp_sys_4::mtp_session>,
seq_id: i32,
) -> Result<Vec<u8>, SpeculativeStateError> {
let mut size = 0_usize;
let status =
unsafe { llama_cpp_sys_4::mtp_session_state_size(raw.as_ptr(), seq_id, &raw mut size) };
status_result(status)?;
validate_size(size)?;
let mut state = vec![0_u8; size];
let mut written = 0_usize;
let status = unsafe {
llama_cpp_sys_4::mtp_session_state_get(
raw.as_ptr(),
seq_id,
state.as_mut_ptr(),
state.len(),
&raw mut written,
)
};
if status == llama_cpp_sys_4::MTP_STATE_STATUS_BUFFER_SMALL {
return Err(SpeculativeStateError::SizeChanged {
expected: size,
actual: written,
});
}
status_result(status)?;
if written != size {
return Err(SpeculativeStateError::SizeChanged {
expected: size,
actual: written,
});
}
Ok(state)
}
pub(crate) fn restore_state(
raw: NonNull<llama_cpp_sys_4::mtp_session>,
seq_id: i32,
state: &[u8],
) -> Result<(), SpeculativeStateError> {
validate_size(state.len())?;
if state.is_empty() {
return Err(SpeculativeStateError::Invalid);
}
let status = unsafe {
llama_cpp_sys_4::mtp_session_state_set(raw.as_ptr(), seq_id, state.as_ptr(), state.len())
};
status_result(status)
}
fn validate_size(size: usize) -> Result<(), SpeculativeStateError> {
if size > MAX_SPECULATIVE_STATE_BYTES {
return Err(SpeculativeStateError::Excessive {
size,
maximum: MAX_SPECULATIVE_STATE_BYTES,
});
}
Ok(())
}
fn status_result(status: llama_cpp_sys_4::mtp_state_status) -> Result<(), SpeculativeStateError> {
match status {
llama_cpp_sys_4::MTP_STATE_STATUS_OK => Ok(()),
llama_cpp_sys_4::MTP_STATE_STATUS_NULL => Err(SpeculativeStateError::Null),
llama_cpp_sys_4::MTP_STATE_STATUS_BAD_SEQUENCE => Err(SpeculativeStateError::BadSequence),
llama_cpp_sys_4::MTP_STATE_STATUS_NOT_QUIESCENT => Err(SpeculativeStateError::NotQuiescent),
llama_cpp_sys_4::MTP_STATE_STATUS_BUFFER_SMALL => {
Err(SpeculativeStateError::BufferTooSmall)
}
llama_cpp_sys_4::MTP_STATE_STATUS_UNAVAILABLE => Err(SpeculativeStateError::Unavailable),
llama_cpp_sys_4::MTP_STATE_STATUS_INVALID => Err(SpeculativeStateError::Invalid),
llama_cpp_sys_4::MTP_STATE_STATUS_OVERFLOW => Err(SpeculativeStateError::Overflow),
llama_cpp_sys_4::MTP_STATE_STATUS_EXCEPTION => Err(SpeculativeStateError::Exception),
unknown => Err(SpeculativeStateError::Unknown(unknown)),
}
}
pub(crate) fn validate_config(
n_seq: u32,
n_draft_max: i32,
n_min: i32,
p_min: f32,
) -> Result<(), &'static str> {
if n_seq == 0 || n_seq > MAX_SPECULATIVE_SEQUENCES {
return Err("n_seq is outside the supported bound");
}
if !(1..=MAX_SPECULATIVE_DRAFT_TOKENS).contains(&n_draft_max) {
return Err("n_draft_max is outside the supported bound");
}
if n_min < 0 || n_min > n_draft_max {
return Err("n_min must be between zero and n_draft_max");
}
if !p_min.is_finite() || !(0.0..=1.0).contains(&p_min) {
return Err("p_min must be finite and between zero and one");
}
Ok(())
}
#[derive(Clone, Copy)]
pub(crate) struct SpeculativeContextCapacity {
pub(crate) batch: u32,
pub(crate) micro_batch: u32,
pub(crate) recurrent_slots: u32,
pub(crate) recurrent_or_hybrid: bool,
}
pub(crate) fn validate_context_capacities(
target: SpeculativeContextCapacity,
draft: SpeculativeContextCapacity,
maximum_draft_tokens: u32,
) -> Result<(), &'static str> {
let required_rows = maximum_draft_tokens
.checked_add(1)
.ok_or("n_draft_max plus one exceeds u32")?;
if target.batch < required_rows || draft.batch < required_rows {
return Err("target or draft batch capacity is smaller than n_draft_max plus one");
}
if (target.recurrent_or_hybrid && target.micro_batch < required_rows)
|| (draft.recurrent_or_hybrid && draft.micro_batch < required_rows)
{
return Err("recurrent micro-batch capacity is smaller than n_draft_max plus one");
}
if (target.recurrent_or_hybrid && target.recurrent_slots < maximum_draft_tokens)
|| (draft.recurrent_or_hybrid && draft.recurrent_slots < maximum_draft_tokens)
{
return Err("recurrent context capacity is smaller than n_draft_max");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_state_is_bounded_before_native_access() {
assert!(validate_size(MAX_SPECULATIVE_STATE_BYTES).is_ok());
assert_eq!(
validate_size(MAX_SPECULATIVE_STATE_BYTES + 1),
Err(SpeculativeStateError::Excessive {
size: MAX_SPECULATIVE_STATE_BYTES + 1,
maximum: MAX_SPECULATIVE_STATE_BYTES,
})
);
}
#[test]
fn speculative_configuration_is_bounded_before_allocation() {
assert!(validate_config(1, 4, 0, 0.0).is_ok());
assert!(validate_config(0, 4, 0, 0.0).is_err());
assert!(validate_config(MAX_SPECULATIVE_SEQUENCES + 1, 4, 0, 0.0).is_err());
assert!(validate_config(1, MAX_SPECULATIVE_DRAFT_TOKENS + 1, 0, 0.0).is_err());
assert!(validate_config(1, 4, 5, 0.0).is_err());
assert!(validate_config(1, 4, 0, f32::NAN).is_err());
assert!(validate_config(1, 4, 0, 1.1).is_err());
}
#[test]
fn prompt_and_state_bounds_are_consistent() {
let maximum_prompt_bytes = MAX_SPECULATIVE_PROMPT_TOKENS
.checked_mul(std::mem::size_of::<i32>())
.unwrap();
assert!(maximum_prompt_bytes < MAX_SPECULATIVE_STATE_BYTES);
}
#[test]
fn speculative_decode_capacity_is_checked_before_native_allocation() {
let transformer = SpeculativeContextCapacity {
batch: 4,
micro_batch: 1,
recurrent_slots: 0,
recurrent_or_hybrid: false,
};
assert!(validate_context_capacities(transformer, transformer, 3).is_ok());
assert!(validate_context_capacities(
SpeculativeContextCapacity {
batch: 3,
..transformer
},
transformer,
3,
)
.is_err());
let recurrent = SpeculativeContextCapacity {
batch: 4,
micro_batch: 4,
recurrent_slots: 3,
recurrent_or_hybrid: true,
};
assert!(validate_context_capacities(recurrent, recurrent, 3).is_ok());
assert!(validate_context_capacities(
SpeculativeContextCapacity {
micro_batch: 3,
..recurrent
},
recurrent,
3,
)
.is_err());
assert!(validate_context_capacities(
SpeculativeContextCapacity {
recurrent_slots: 2,
..recurrent
},
recurrent,
3,
)
.is_err());
assert!(validate_context_capacities(transformer, transformer, u32::MAX).is_err());
}
}