use std::marker::PhantomData;
use std::ptr::NonNull;
use std::rc::Rc;
use crate::context::params::LlamaContextType;
use crate::context::LlamaContext;
use crate::llama_batch::LlamaBatch;
use crate::speculative::MAX_SPECULATIVE_PROMPT_TOKENS;
use crate::speculative::{
capture_state, restore_state, validate_config, validate_context_capacities,
SpeculativeContextCapacity, SpeculativeStateError,
};
use crate::token::LlamaToken;
#[derive(Debug, thiserror::Error)]
pub enum MtpSessionError {
#[error("failed to create MTP draft session — check that ctx_dft was built with LlamaContextType::Mtp and the model has MTP heads")]
Init,
#[error("mtp_session_process failed (see llama.cpp logs)")]
Process,
#[error("mtp_session_begin failed")]
Begin,
#[error("mtp_session_draft failed")]
Draft,
#[error("mtp_session_accept failed")]
Accept,
#[error("prompt has {size} tokens, exceeding the {maximum}-token bound")]
PromptTooLong {
size: usize,
maximum: usize,
},
#[error("incompatible MTP contexts: {0}")]
IncompatibleContexts(&'static str),
#[error("sequence id {seq_id} out of range (n_seq = {n_seq})")]
BadSeqId {
seq_id: i32,
n_seq: u32,
},
#[error("invalid MTP session config: {0}")]
InvalidConfig(&'static str),
#[error("target decode failed: {0}")]
Decode(#[from] crate::DecodeError),
#[error("sequence {seq_id} still has an unaccepted draft proposal")]
ProposalPending {
seq_id: i32,
},
#[error("sequence {seq_id} has no draft proposal to accept")]
NoPendingProposal {
seq_id: i32,
},
#[error("accepted {accepted} tokens from a {proposed}-token proposal")]
AcceptedTooMany {
accepted: u16,
proposed: usize,
},
#[error(transparent)]
State(#[from] SpeculativeStateError),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MtpSessionConfig {
pub n_seq: u32,
pub n_draft_max: i32,
pub n_min: i32,
pub p_min: f32,
}
impl MtpSessionConfig {
#[must_use]
pub fn new(n_seq: u32, n_draft_max: i32) -> Self {
Self {
n_seq,
n_draft_max,
n_min: 0,
p_min: 0.0,
}
}
#[must_use]
pub fn with_n_min(mut self, n_min: i32) -> Self {
self.n_min = n_min;
self
}
#[must_use]
pub fn with_p_min(mut self, p_min: f32) -> Self {
self.p_min = p_min;
self
}
}
pub struct MtpSession<'ctx, 'model> {
raw: NonNull<llama_cpp_sys_4::mtp_session>,
config: MtpSessionConfig,
target: &'ctx mut LlamaContext<'model>,
draft: &'ctx mut LlamaContext<'model>,
pending_proposals: Vec<Option<usize>>,
not_send_sync: PhantomData<Rc<()>>,
}
impl<'ctx, 'model> MtpSession<'ctx, 'model> {
pub fn new(
target: &'ctx mut LlamaContext<'model>,
draft: &'ctx mut LlamaContext<'model>,
n_seq: u32,
n_draft_max: i32,
) -> Result<Self, MtpSessionError> {
Self::new_with_config(target, draft, MtpSessionConfig::new(n_seq, n_draft_max))
}
pub fn new_with_config(
target: &'ctx mut LlamaContext<'model>,
draft: &'ctx mut LlamaContext<'model>,
config: MtpSessionConfig,
) -> Result<Self, MtpSessionError> {
validate_config(config.n_seq, config.n_draft_max, config.n_min, config.p_min)
.map_err(MtpSessionError::InvalidConfig)?;
validate_contexts(target, draft, config)?;
let sequence_slots = usize::try_from(config.n_seq)
.map_err(|_| MtpSessionError::InvalidConfig("n_seq exceeds usize"))?;
#[allow(clippy::cast_possible_wrap)]
let c_config = llama_cpp_sys_4::mtp_session_config {
n_seq: config.n_seq,
n_draft_max: config.n_draft_max,
n_min: config.n_min,
p_min: config.p_min,
spec_type: llama_cpp_sys_4::MTP_SPEC_TYPE_MTP as i32,
};
let raw = unsafe {
llama_cpp_sys_4::mtp_session_new(
target.context.as_ptr(),
draft.context.as_ptr(),
&raw const c_config,
)
};
let raw = NonNull::new(raw).ok_or(MtpSessionError::Init)?;
Ok(Self {
raw,
config,
target,
draft,
pending_proposals: vec![None; sequence_slots],
not_send_sync: PhantomData,
})
}
#[must_use]
pub fn config(&self) -> MtpSessionConfig {
self.config
}
#[must_use]
pub fn need_embd(&self) -> bool {
unsafe { llama_cpp_sys_4::mtp_session_need_embd(self.raw.as_ptr()) }
}
#[must_use]
pub fn need_embd_pre_norm(&self) -> bool {
unsafe { llama_cpp_sys_4::mtp_session_need_embd_pre_norm(self.raw.as_ptr()) }
}
#[must_use]
pub fn n_draft_max(&self) -> i32 {
self.config.n_draft_max
}
#[must_use]
pub fn n_min(&self) -> i32 {
self.config.n_min
}
#[must_use]
pub fn p_min(&self) -> f32 {
self.config.p_min
}
#[must_use]
pub fn n_seq(&self) -> u32 {
self.config.n_seq
}
#[must_use]
pub fn target_context(&self) -> &LlamaContext<'model> {
self.target
}
#[must_use]
pub fn target_context_mut(&mut self) -> &mut LlamaContext<'model> {
self.target
}
#[must_use]
pub fn draft_context(&self) -> &LlamaContext<'model> {
self.draft
}
#[must_use]
pub fn draft_context_mut(&mut self) -> &mut LlamaContext<'model> {
self.draft
}
pub fn decode_target_and_process(
&mut self,
batch: &mut LlamaBatch,
) -> Result<(), MtpSessionError> {
self.decode_target(batch)?;
self.process(batch)
}
pub fn decode_target(&mut self, batch: &mut LlamaBatch) -> Result<(), MtpSessionError> {
self.target.decode(batch)?;
Ok(())
}
pub fn print_stats(&self) {
unsafe { llama_cpp_sys_4::mtp_session_print_stats(self.raw.as_ptr()) }
}
pub fn begin(&mut self, seq_id: i32, prompt: &[LlamaToken]) -> Result<(), MtpSessionError> {
self.check_seq(seq_id)?;
self.require_quiescent()?;
if prompt.len() > MAX_SPECULATIVE_PROMPT_TOKENS {
return Err(MtpSessionError::PromptTooLong {
size: prompt.len(),
maximum: MAX_SPECULATIVE_PROMPT_TOKENS,
});
}
let ok = unsafe {
llama_cpp_sys_4::mtp_session_begin(
self.raw.as_ptr(),
seq_id,
prompt.as_ptr().cast(),
prompt.len(),
)
};
if !ok {
return Err(MtpSessionError::Begin);
}
Ok(())
}
pub fn process(&mut self, batch: &LlamaBatch) -> Result<(), MtpSessionError> {
let ok = unsafe {
llama_cpp_sys_4::mtp_session_process(self.raw.as_ptr(), &raw const batch.llama_batch)
};
if ok {
Ok(())
} else {
Err(MtpSessionError::Process)
}
}
pub fn draft(
&mut self,
seq_id: i32,
n_past: i32,
id_last: LlamaToken,
) -> Result<Vec<LlamaToken>, MtpSessionError> {
self.check_seq(seq_id)?;
let sequence_index = self.sequence_index(seq_id)?;
if self.pending_proposals[sequence_index].is_some() {
return Err(MtpSessionError::ProposalPending { seq_id });
}
let cap = usize::try_from(self.config.n_draft_max.max(0)).unwrap_or(0);
let mut buf: Vec<i32> = vec![0; cap];
let mut out_n = i32::try_from(cap).unwrap_or(i32::MAX);
let ok = unsafe {
llama_cpp_sys_4::mtp_session_draft(
self.raw.as_ptr(),
seq_id,
n_past,
id_last.0,
buf.as_mut_ptr(),
&raw mut out_n,
)
};
if !ok {
return Err(MtpSessionError::Draft);
}
let n = usize::try_from(out_n.max(0)).unwrap_or(0);
buf.truncate(n);
if n > 0 {
self.pending_proposals[sequence_index] = Some(n);
}
Ok(buf.into_iter().map(LlamaToken).collect())
}
pub fn accept(&mut self, seq_id: i32, n_accepted: u16) -> Result<(), MtpSessionError> {
self.check_seq(seq_id)?;
let sequence_index = self.sequence_index(seq_id)?;
let proposed = self.pending_proposals[sequence_index]
.ok_or(MtpSessionError::NoPendingProposal { seq_id })?;
if usize::from(n_accepted) > proposed {
return Err(MtpSessionError::AcceptedTooMany {
accepted: n_accepted,
proposed,
});
}
let ok =
unsafe { llama_cpp_sys_4::mtp_session_accept(self.raw.as_ptr(), seq_id, n_accepted) };
if !ok {
return Err(MtpSessionError::Accept);
}
self.pending_proposals[sequence_index] = None;
Ok(())
}
#[must_use]
pub fn is_quiescent(&self) -> bool {
self.pending_proposals.iter().all(Option::is_none)
&& unsafe { llama_cpp_sys_4::mtp_session_is_quiescent(self.raw.as_ptr()) }
}
pub fn speculative_state(&self, seq_id: i32) -> Result<Vec<u8>, MtpSessionError> {
self.check_seq(seq_id)?;
self.require_quiescent()?;
Ok(capture_state(self.raw, seq_id)?)
}
pub fn restore_speculative_state(
&mut self,
seq_id: i32,
state: &[u8],
) -> Result<(), MtpSessionError> {
self.check_seq(seq_id)?;
self.require_quiescent()?;
restore_state(self.raw, seq_id, state)?;
Ok(())
}
pub fn clear_target_kv_cache_seq(
&mut self,
seq_id: Option<u32>,
p0: Option<u32>,
p1: Option<u32>,
) -> Result<bool, crate::context::kv_cache::KvCacheConversionError> {
self.target.clear_kv_cache_seq(seq_id, p0, p1)
}
pub fn clear_draft_kv_cache_seq(
&mut self,
seq_id: Option<u32>,
p0: Option<u32>,
p1: Option<u32>,
) -> Result<bool, crate::context::kv_cache::KvCacheConversionError> {
self.draft.clear_kv_cache_seq(seq_id, p0, p1)
}
pub fn target_state_seq_get_size_ext(&mut self, seq_id: i32, flags: u32) -> usize {
self.target.state_seq_get_size_ext(seq_id, flags)
}
pub fn target_state_seq_get_data_ext(
&mut self,
dst: &mut [u8],
seq_id: i32,
flags: u32,
) -> usize {
self.target.state_seq_get_data_ext(dst, seq_id, flags)
}
pub fn target_state_seq_set_data_ext(&mut self, src: &[u8], seq_id: i32, flags: u32) -> usize {
self.target.state_seq_set_data_ext(src, seq_id, flags)
}
pub fn draft_state_seq_get_size_ext(&mut self, seq_id: i32, flags: u32) -> usize {
self.draft.state_seq_get_size_ext(seq_id, flags)
}
pub fn draft_state_seq_get_data_ext(
&mut self,
dst: &mut [u8],
seq_id: i32,
flags: u32,
) -> usize {
self.draft.state_seq_get_data_ext(dst, seq_id, flags)
}
pub fn draft_state_seq_set_data_ext(&mut self, src: &[u8], seq_id: i32, flags: u32) -> usize {
self.draft.state_seq_set_data_ext(src, seq_id, flags)
}
fn require_quiescent(&self) -> Result<(), MtpSessionError> {
if let Some((index, _)) = self
.pending_proposals
.iter()
.enumerate()
.find(|(_, proposal)| proposal.is_some())
{
return Err(MtpSessionError::ProposalPending {
seq_id: i32::try_from(index).unwrap_or(i32::MAX),
});
}
if !unsafe { llama_cpp_sys_4::mtp_session_is_quiescent(self.raw.as_ptr()) } {
return Err(MtpSessionError::State(SpeculativeStateError::NotQuiescent));
}
Ok(())
}
fn check_seq(&self, seq_id: i32) -> Result<(), MtpSessionError> {
if seq_id < 0 || seq_id.cast_unsigned() >= self.config.n_seq {
return Err(MtpSessionError::BadSeqId {
seq_id,
n_seq: self.config.n_seq,
});
}
Ok(())
}
fn sequence_index(&self, seq_id: i32) -> Result<usize, MtpSessionError> {
self.check_seq(seq_id)?;
usize::try_from(seq_id)
.map_err(|_| MtpSessionError::InvalidConfig("sequence id exceeds usize"))
}
}
fn validate_contexts(
target: &LlamaContext<'_>,
draft: &LlamaContext<'_>,
config: MtpSessionConfig,
) -> Result<(), MtpSessionError> {
if target.context_type() != LlamaContextType::Default
|| draft.context_type() != LlamaContextType::Mtp
{
return Err(MtpSessionError::IncompatibleContexts(
"target must be Default and draft must be Mtp",
));
}
if target.n_seq_max() < config.n_seq || draft.n_seq_max() != config.n_seq {
return Err(MtpSessionError::IncompatibleContexts(
"target sequence capacity is too small or draft capacity differs from n_seq",
));
}
let required_draft = u32::try_from(config.n_draft_max)
.map_err(|_| MtpSessionError::InvalidConfig("n_draft_max exceeds u32"))?;
validate_context_capacities(
SpeculativeContextCapacity {
batch: target.n_batch(),
micro_batch: target.n_ubatch(),
recurrent_slots: target.n_rs_seq(),
recurrent_or_hybrid: target.model.is_recurrent() || target.model.is_hybrid(),
},
SpeculativeContextCapacity {
batch: draft.n_batch(),
micro_batch: draft.n_ubatch(),
recurrent_slots: draft.n_rs_seq(),
recurrent_or_hybrid: draft.model.is_recurrent() || draft.model.is_hybrid(),
},
required_draft,
)
.map_err(MtpSessionError::IncompatibleContexts)?;
if draft.model.n_embd_out() != target.model.n_embd() {
return Err(MtpSessionError::IncompatibleContexts(
"draft output width differs from target hidden width",
));
}
Ok(())
}
impl Drop for MtpSession<'_, '_> {
fn drop(&mut self) {
unsafe { llama_cpp_sys_4::mtp_session_free(self.raw.as_ptr()) }
}
}
impl std::fmt::Debug for MtpSession<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MtpSession")
.field("config", &self.config)
.field("need_embd_pre_norm", &self.need_embd_pre_norm())
.finish_non_exhaustive()
}
}