#![deny(clippy::undocumented_unsafe_blocks)]
use std::ptr::NonNull;
use llama_cpp_sys_4::{llama_pos, llama_seq_id};
use crate::context::LlamaContext;
use crate::token::LlamaToken;
pub const MROPE_SECTIONS: usize = llama_cpp_sys_4::GGML_MROPE_SECTIONS as usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BatchEntry(i32);
impl BatchEntry {
#[must_use]
pub fn index(self) -> i32 {
self.0
}
}
impl From<BatchEntry> for i32 {
fn from(entry: BatchEntry) -> Self {
entry.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct EmbeddingRows<'a> {
data: &'a [f32],
n_rows: usize,
n_embd: usize,
}
impl<'a> EmbeddingRows<'a> {
pub fn new(data: &'a [f32], n_rows: usize, n_embd: usize) -> Result<Self, BatchExtError> {
if n_rows.checked_mul(n_embd) != Some(data.len()) {
return Err(BatchExtError::EmbeddingShape {
n_rows,
n_embd,
len: data.len(),
});
}
Ok(Self {
data,
n_rows,
n_embd,
})
}
#[must_use]
pub fn row(data: &'a [f32]) -> Self {
Self {
data,
n_rows: 1,
n_embd: data.len(),
}
}
#[must_use]
pub fn n_rows(&self) -> usize {
self.n_rows
}
#[must_use]
pub fn n_embd(&self) -> usize {
self.n_embd
}
fn total(self) -> usize {
self.data.len()
}
fn as_raw(self) -> llama_cpp_sys_4::llama_embd {
llama_cpp_sys_4::llama_embd {
data: self.data.as_ptr(),
n_rows: self.n_rows,
n_embd: self.n_embd,
}
}
}
#[allow(clippy::unnecessary_cast)]
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LlamaProcessType {
Encode = llama_cpp_sys_4::LLAMA_PROCESS_TYPE_ENCODE as u32,
Decode = llama_cpp_sys_4::LLAMA_PROCESS_TYPE_DECODE as u32,
}
impl From<LlamaProcessType> for llama_cpp_sys_4::llama_process_type {
fn from(value: LlamaProcessType) -> Self {
value as u32 as Self
}
}
#[allow(clippy::unnecessary_cast)]
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LlamaDecisionOrder {
#[default]
None = llama_cpp_sys_4::LLAMA_DECISION_ORDER_NONE as u32,
QuestionNoul = llama_cpp_sys_4::LLAMA_DECISION_ORDER_QUESTION_NOUL as u32,
QuestionChoice = llama_cpp_sys_4::LLAMA_DECISION_ORDER_QUESTION_CHOICE as u32,
QuestionScore = llama_cpp_sys_4::LLAMA_DECISION_ORDER_QUESTION_SCORE as u32,
Option = llama_cpp_sys_4::LLAMA_DECISION_ORDER_OPTION as u32,
}
impl From<LlamaDecisionOrder> for llama_cpp_sys_4::llama_decision_order {
fn from(value: LlamaDecisionOrder) -> Self {
value as u32 as Self
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BatchExtError {
#[error("llama.cpp returned null from llama_batch_ext_init")]
NullReturn,
#[error("batch is full at {0} entries (the context's n_batch)")]
Full(u32),
#[error("token id {0} is out of range for this model's vocabulary")]
InvalidToken(i32),
#[error("sequence id {seq_id} is out of range (n_seq_max = {n_seq_max})")]
InvalidSeqId {
seq_id: llama_seq_id,
n_seq_max: u32,
},
#[error("embedding shape {n_rows} x {n_embd} does not match a buffer of {len} values")]
EmbeddingShape {
n_rows: usize,
n_embd: usize,
len: usize,
},
#[error("entry {entry} is not in this batch ({len} entries)")]
NoSuchEntry {
entry: i32,
len: usize,
},
#[error("entry {entry} needs exactly {expected} position(s), got {got}")]
PositionArity {
entry: i32,
expected: usize,
got: usize,
},
#[error("this batch's embeddings are {expected} values wide, got {got}")]
EmbeddingWidthMismatch {
expected: usize,
got: usize,
},
#[error("entry {0} already has an input embedding")]
EmbeddingAlreadySet(i32),
#[error("entry {0} already has a state embedding")]
StateAlreadySet(i32),
#[error("llama.cpp rejected the embedding (see the llama.cpp log for the reason)")]
EmbeddingRejected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Entry {
has_token: bool,
has_embd: bool,
has_state: bool,
}
impl Entry {
fn token() -> Self {
Self {
has_token: true,
has_embd: false,
has_state: false,
}
}
fn embedding() -> Self {
Self {
has_token: false,
has_embd: true,
has_state: false,
}
}
fn empty() -> Self {
Self {
has_token: false,
has_embd: false,
has_state: false,
}
}
fn is_incomplete(self) -> bool {
!self.has_token && !self.has_embd
}
fn n_pos(self, n_pos_per_embd: usize) -> usize {
if self.has_token {
1
} else {
n_pos_per_embd
}
}
}
#[derive(Debug)]
pub struct LlamaBatchExt {
raw: NonNull<llama_cpp_sys_4::llama_batch_ext>,
pub(crate) ctx_id: u64,
entries: Vec<Entry>,
pub(crate) initialized_logits: Vec<i32>,
embd_width: Option<usize>,
n_pos_per_embd: usize,
n_batch: u32,
n_seq_max: u32,
n_vocab: i32,
}
unsafe impl Send for LlamaBatchExt {}
impl LlamaBatchExt {
pub fn new(ctx: &LlamaContext<'_>) -> Result<Self, BatchExtError> {
let raw = unsafe { llama_cpp_sys_4::llama_batch_ext_init(ctx.context.as_ptr()) };
let raw = NonNull::new(raw).ok_or(BatchExtError::NullReturn)?;
let rope_type = ctx.model.rope_type();
let n_pos_per_embd = if rope_type == llama_cpp_sys_4::LLAMA_ROPE_TYPE_MROPE
|| rope_type == llama_cpp_sys_4::LLAMA_ROPE_TYPE_IMROPE
{
MROPE_SECTIONS
} else {
1
};
Ok(Self {
raw,
ctx_id: ctx.id(),
entries: Vec::new(),
initialized_logits: Vec::new(),
embd_width: None,
n_pos_per_embd,
n_batch: ctx.n_batch(),
n_seq_max: ctx.n_seq_max(),
n_vocab: ctx.model.n_vocab(),
})
}
pub fn clear(&mut self) {
unsafe { llama_cpp_sys_4::llama_batch_ext_clear(self.raw.as_ptr()) }
self.entries.clear();
self.initialized_logits.clear();
self.embd_width = None;
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn has_incomplete_entry(&self) -> bool {
self.entries.iter().any(|entry| entry.is_incomplete())
}
pub(crate) fn first_incomplete_entry(&self) -> Option<i32> {
let idx = self
.entries
.iter()
.position(|entry| entry.is_incomplete())?;
i32::try_from(idx).ok()
}
#[must_use]
pub fn capacity(&self) -> u32 {
self.n_batch
}
#[must_use]
pub fn n_pos_per_embd(&self) -> usize {
self.n_pos_per_embd
}
#[must_use]
pub fn outputs(&self) -> &[i32] {
&self.initialized_logits
}
pub fn add_token(
&mut self,
LlamaToken(id): LlamaToken,
seq_id: llama_seq_id,
) -> Result<BatchEntry, BatchExtError> {
if id < 0 || id >= self.n_vocab {
return Err(BatchExtError::InvalidToken(id));
}
let idx =
unsafe { llama_cpp_sys_4::llama_batch_ext_add_token(self.raw.as_ptr(), seq_id, id) };
self.finish_add(idx, seq_id, Entry::token(), BatchExtError::InvalidToken(id))
}
pub fn add_embedding(
&mut self,
embd: EmbeddingRows<'_>,
seq_id: llama_seq_id,
) -> Result<BatchEntry, BatchExtError> {
self.check_embd_width(embd)?;
let idx = unsafe {
llama_cpp_sys_4::llama_batch_ext_add_embd(self.raw.as_ptr(), seq_id, embd.as_raw())
};
let entry = self.finish_add(
idx,
seq_id,
Entry::embedding(),
BatchExtError::EmbeddingRejected,
);
if entry.is_ok() {
self.embd_width = Some(embd.total());
}
entry
}
pub fn add_empty(&mut self, seq_id: llama_seq_id) -> Result<BatchEntry, BatchExtError> {
let idx = unsafe { llama_cpp_sys_4::llama_batch_ext_add(self.raw.as_ptr(), seq_id) };
self.finish_add(
idx,
seq_id,
Entry::empty(),
BatchExtError::Full(self.n_batch),
)
}
fn finish_add(
&mut self,
idx: i32,
seq_id: llama_seq_id,
entry: Entry,
rejected: BatchExtError,
) -> Result<BatchEntry, BatchExtError> {
match idx {
-1 => Err(BatchExtError::Full(self.n_batch)),
-3 => Err(BatchExtError::InvalidSeqId {
seq_id,
n_seq_max: self.n_seq_max,
}),
-2 => {
self.entries.push(Entry::empty());
Err(rejected)
}
idx if idx < 0 => Err(rejected),
idx => {
debug_assert_eq!(usize::try_from(idx).ok(), Some(self.entries.len()));
self.entries.push(entry);
Ok(BatchEntry(idx))
}
}
}
pub fn add_seq(
&mut self,
entry: BatchEntry,
seq_id: llama_seq_id,
) -> Result<(), BatchExtError> {
self.check_entry(entry)?;
let ok =
unsafe { llama_cpp_sys_4::llama_batch_ext_add_seq(self.raw.as_ptr(), entry.0, seq_id) };
if ok {
Ok(())
} else {
Err(BatchExtError::InvalidSeqId {
seq_id,
n_seq_max: self.n_seq_max,
})
}
}
pub fn set_pos(&mut self, entry: BatchEntry, pos: llama_pos) -> Result<(), BatchExtError> {
self.set_positions(entry, &[pos])
}
pub fn set_positions(
&mut self,
entry: BatchEntry,
pos: &[llama_pos],
) -> Result<(), BatchExtError> {
let expected = self.check_entry(entry)?.n_pos(self.n_pos_per_embd);
if pos.len() != expected {
return Err(BatchExtError::PositionArity {
entry: entry.0,
expected,
got: pos.len(),
});
}
let ok = unsafe {
llama_cpp_sys_4::llama_batch_ext_set_pos(self.raw.as_ptr(), entry.0, pos.as_ptr())
};
debug_assert!(ok, "set_pos rejected an entry that check_entry accepted");
Ok(())
}
pub fn set_token_embedding(
&mut self,
entry: BatchEntry,
embd: EmbeddingRows<'_>,
) -> Result<(), BatchExtError> {
if self.check_entry(entry)?.has_embd {
return Err(BatchExtError::EmbeddingAlreadySet(entry.0));
}
self.check_embd_width(embd)?;
let ok = unsafe {
llama_cpp_sys_4::llama_batch_ext_set_embd_token(
self.raw.as_ptr(),
entry.0,
embd.as_raw(),
)
};
if !ok {
return Err(BatchExtError::EmbeddingRejected);
}
self.embd_width = Some(embd.total());
self.mark(entry, |slot| slot.has_embd = true);
Ok(())
}
pub fn set_state_embedding(
&mut self,
entry: BatchEntry,
state: EmbeddingRows<'_>,
) -> Result<(), BatchExtError> {
if self.check_entry(entry)?.has_state {
return Err(BatchExtError::StateAlreadySet(entry.0));
}
let ok = unsafe {
llama_cpp_sys_4::llama_batch_ext_set_embd_state(
self.raw.as_ptr(),
entry.0,
state.as_raw(),
)
};
if !ok {
return Err(BatchExtError::EmbeddingRejected);
}
self.mark(entry, |slot| slot.has_state = true);
Ok(())
}
pub fn set_output(&mut self, entry: BatchEntry, value: bool) -> Result<(), BatchExtError> {
self.check_entry(entry)?;
let ok = unsafe {
llama_cpp_sys_4::llama_batch_ext_set_output_logits(self.raw.as_ptr(), entry.0, value)
};
debug_assert!(ok, "set_output rejected an entry that check_entry accepted");
if value {
if !self.initialized_logits.contains(&entry.0) {
self.initialized_logits.push(entry.0);
}
} else {
self.initialized_logits.retain(|i| *i != entry.0);
}
Ok(())
}
pub fn set_decision_order(
&mut self,
entry: BatchEntry,
order: LlamaDecisionOrder,
) -> Result<(), BatchExtError> {
self.check_entry(entry)?;
let ok = unsafe {
llama_cpp_sys_4::llama_batch_ext_set_decision_order(
self.raw.as_ptr(),
entry.0,
order.into(),
)
};
debug_assert!(
ok,
"set_decision_order rejected an entry that check_entry accepted"
);
Ok(())
}
fn check_entry(&self, entry: BatchEntry) -> Result<Entry, BatchExtError> {
usize::try_from(entry.0)
.ok()
.and_then(|idx| self.entries.get(idx))
.copied()
.ok_or(BatchExtError::NoSuchEntry {
entry: entry.0,
len: self.entries.len(),
})
}
fn check_embd_width(&self, embd: EmbeddingRows<'_>) -> Result<(), BatchExtError> {
match self.embd_width {
Some(expected) if expected != embd.total() => {
Err(BatchExtError::EmbeddingWidthMismatch {
expected,
got: embd.total(),
})
}
_ => Ok(()),
}
}
fn mark(&mut self, entry: BatchEntry, update: impl FnOnce(&mut Entry)) {
if let Ok(idx) = usize::try_from(entry.0) {
if let Some(slot) = self.entries.get_mut(idx) {
update(slot);
}
}
}
pub(crate) fn as_ptr(&mut self) -> *mut llama_cpp_sys_4::llama_batch_ext {
self.raw.as_ptr()
}
}
impl Drop for LlamaBatchExt {
fn drop(&mut self) {
unsafe { llama_cpp_sys_4::llama_batch_ext_free(self.raw.as_ptr()) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedding_rows_rejects_a_shape_that_does_not_match_the_buffer() {
let data = [0.0_f32; 6];
assert_eq!(
EmbeddingRows::new(&data, 2, 4).unwrap_err(),
BatchExtError::EmbeddingShape {
n_rows: 2,
n_embd: 4,
len: 6
}
);
let rows = EmbeddingRows::new(&data, 2, 3).unwrap();
assert_eq!((rows.n_rows(), rows.n_embd()), (2, 3));
}
#[test]
fn embedding_rows_row_is_one_row_of_everything() {
let data = [1.0_f32, 2.0, 3.0];
let rows = EmbeddingRows::row(&data);
assert_eq!((rows.n_rows(), rows.n_embd()), (1, 3));
}
#[test]
fn embedding_rows_shape_does_not_wrap() {
let data = [0.0_f32; 1];
assert!(EmbeddingRows::new(&data, usize::MAX, 2).is_err());
}
#[test]
fn mrope_sections_matches_upstream() {
assert_eq!(MROPE_SECTIONS, 4);
}
#[test]
fn process_type_maps_onto_the_sys_constants() {
assert_eq!(
llama_cpp_sys_4::llama_process_type::from(LlamaProcessType::Encode),
llama_cpp_sys_4::LLAMA_PROCESS_TYPE_ENCODE
);
assert_eq!(
llama_cpp_sys_4::llama_process_type::from(LlamaProcessType::Decode),
llama_cpp_sys_4::LLAMA_PROCESS_TYPE_DECODE
);
}
#[test]
fn decision_order_maps_onto_the_sys_constants() {
assert_eq!(
llama_cpp_sys_4::llama_decision_order::from(LlamaDecisionOrder::None),
llama_cpp_sys_4::LLAMA_DECISION_ORDER_NONE
);
assert_eq!(
llama_cpp_sys_4::llama_decision_order::from(LlamaDecisionOrder::Option),
llama_cpp_sys_4::LLAMA_DECISION_ORDER_OPTION
);
}
}