Skip to main content

llama_cpp_2/
llama_batch.rs

1//! Safe wrapper around `llama_batch`.
2
3use crate::token::LlamaToken;
4use llama_cpp_sys_2::{llama_batch, llama_batch_free, llama_batch_init, llama_pos, llama_seq_id};
5use std::marker::PhantomData;
6
7/// A safe wrapper around `llama_batch`.
8#[derive(Debug)]
9pub struct LlamaBatch<'a> {
10    /// The number of tokens the batch was allocated with. they are safe to write to - but not necessarily read from as they are not necessarily initialized
11    allocated: usize,
12    #[allow(clippy::doc_markdown)]
13    /// The llama_cpp batch. always initialize by `llama_cpp_sys_2::llama_batch_init(allocated, <unknown>, <unknown>)`
14    pub(crate) llama_batch: llama_batch,
15    phantom: PhantomData<&'a [LlamaToken]>,
16}
17
18// SAFETY: The batch is POD, with getters using `&` and setters using `&mut`.
19unsafe impl Send for LlamaBatch<'_> {}
20// SAFETY: Same as above.
21unsafe impl Sync for LlamaBatch<'_> {}
22
23/// Errors that can occur when adding a token to a batch.
24#[derive(thiserror::Error, Debug, PartialEq, Eq)]
25pub enum BatchAddError {
26    /// There was not enough space in the batch to add the token.
27    #[error("Insufficient Space of {0}")]
28    InsufficientSpace(usize),
29    /// Empty buffer is provided for [`LlamaBatch::get_one`]
30    #[error("Empty buffer")]
31    EmptyBuffer,
32}
33
34impl<'a> LlamaBatch<'a> {
35    /// Clear the batch. This does not free the memory associated with the batch, but it does reset
36    /// the number of tokens to 0.
37    pub fn clear(&mut self) {
38        self.llama_batch.n_tokens = 0;
39    }
40
41    /// add a token to the batch for sequences `seq_ids` at position `pos`. If `logits` is true, the
42    /// token will be initialized and can be read from after the next decode.
43    ///
44    /// # Panics
45    ///
46    /// - [`self.llama_batch.n_tokens`] does not fit into a usize
47    /// - [`seq_ids.len()`] does not fit into a [`llama_seq_id`]
48    ///
49    /// # Errors
50    ///
51    /// returns a error if there is insufficient space in the buffer
52    pub fn add(
53        &mut self,
54        LlamaToken(id): LlamaToken,
55        pos: llama_pos,
56        seq_ids: &[i32],
57        logits: bool,
58    ) -> Result<(), BatchAddError> {
59        if self.allocated
60            < usize::try_from(self.n_tokens() + 1).expect("cannot fit n_tokens into a usize")
61        {
62            return Err(BatchAddError::InsufficientSpace(self.allocated));
63        }
64        let offset = self.llama_batch.n_tokens;
65        let offset_usize = usize::try_from(offset).expect("cannot fit n_tokens into a usize");
66        unsafe {
67            // batch.token   [batch.n_tokens] = id;
68            self.llama_batch.token.add(offset_usize).write(id);
69            // batch.pos     [batch.n_tokens] = pos,
70            self.llama_batch.pos.add(offset_usize).write(pos);
71            // batch.n_seq_id[batch.n_tokens] = seq_ids.size();
72            self.llama_batch.n_seq_id.add(offset_usize).write(
73                llama_seq_id::try_from(seq_ids.len())
74                    .expect("cannot fit seq_ids.len() into a llama_seq_id"),
75            );
76            // for (size_t i = 0; i < seq_ids.size(); ++i) {
77            //     batch.seq_id[batch.n_tokens][i] = seq_ids[i];
78            // }
79            for (i, seq_id) in seq_ids.iter().enumerate() {
80                let tmp = *self.llama_batch.seq_id.add(offset_usize);
81                tmp.add(i).write(*seq_id);
82            }
83            // batch.logits  [batch.n_tokens] = logits;
84            self.llama_batch
85                .logits
86                .add(offset_usize)
87                .write(i8::from(logits));
88        }
89
90        // batch.n_tokens++;
91        self.llama_batch.n_tokens += 1;
92
93        Ok(())
94    }
95
96    /// Add a sequence of tokens to the batch for the given sequence id. If `logits_all` is true, the
97    /// tokens will be initialized and can be read from after the next decode.
98    ///
99    /// Either way the last token in the sequence will have its logits set to `true`.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if there is insufficient space in the buffer
104    ///
105    /// # Panics
106    ///
107    /// - [`self.llama_batch.n_tokens`] does not fit into a [`usize`]
108    /// - [`n_tokens - 1`] does not fit into a [`llama_pos`]
109    pub fn add_sequence(
110        &mut self,
111        tokens: &[LlamaToken],
112        seq_id: i32,
113        logits_all: bool,
114    ) -> Result<(), BatchAddError> {
115        let n_tokens_0 =
116            usize::try_from(self.llama_batch.n_tokens).expect("cannot fit n_tokens into a usize");
117        let n_tokens = tokens.len();
118
119        if self.allocated < n_tokens_0 + n_tokens {
120            return Err(BatchAddError::InsufficientSpace(self.allocated));
121        }
122
123        let last_index = llama_pos::try_from(n_tokens.saturating_sub(1))
124            .expect("cannot fit n_tokens into a llama_pos");
125        for (i, token) in (0..).zip(tokens.iter()) {
126            self.add(*token, i, &[seq_id], logits_all || i == last_index)?;
127        }
128
129        Ok(())
130    }
131
132    /// Create a new `LlamaBatch` that can contain up to `n_tokens` tokens.
133    ///
134    /// # Arguments
135    ///
136    /// - `n_tokens`: the maximum number of tokens that can be added to the batch
137    /// - `n_seq_max`: the maximum number of sequences that can be added to the batch (generally 1 unless you know what you are doing)
138    ///
139    /// # Panics
140    ///
141    /// Panics if `n_tokens` is greater than `i32::MAX`.
142    #[must_use]
143    pub fn new(n_tokens: usize, n_seq_max: i32) -> Self {
144        let n_tokens_i32 = i32::try_from(n_tokens).expect("cannot fit n_tokens into a i32");
145        let batch = unsafe { llama_batch_init(n_tokens_i32, 0, n_seq_max) };
146
147        LlamaBatch {
148            allocated: n_tokens,
149            llama_batch: batch,
150            phantom: PhantomData,
151        }
152    }
153
154    /// ``llama_batch_get_one``
155    /// Return batch for single sequence of tokens
156    ///
157    /// NOTE: this is a helper function to facilitate transition to the new batch API
158    ///
159    /// # Errors
160    /// If the provided token buffer is empty.
161    ///
162    /// # Panics
163    /// If the number of tokens in ``tokens`` exceeds [`i32::MAX`].
164    pub fn get_one(tokens: &'a [LlamaToken]) -> Result<Self, BatchAddError> {
165        if tokens.is_empty() {
166            return Err(BatchAddError::EmptyBuffer);
167        }
168        let batch = unsafe {
169            let ptr = tokens.as_ptr() as *mut i32;
170            llama_cpp_sys_2::llama_batch_get_one(
171                ptr,
172                tokens
173                    .len()
174                    .try_into()
175                    .expect("number of tokens exceeds i32::MAX"),
176            )
177        };
178        let batch = Self {
179            allocated: 0,
180            llama_batch: batch,
181            phantom: PhantomData,
182        };
183        Ok(batch)
184    }
185
186    /// Returns the number of tokens in the batch.
187    #[must_use]
188    pub fn n_tokens(&self) -> i32 {
189        self.llama_batch.n_tokens
190    }
191}
192
193impl<'a> Drop for LlamaBatch<'a> {
194    /// Drops the `LlamaBatch`.
195    ///
196    /// ```
197    /// # use llama_cpp_2::llama_batch::LlamaBatch;
198    /// # use std::error::Error;
199    /// # fn main() -> Result<(), Box<dyn Error>> {
200    /// let batch = LlamaBatch::new(512, 1);
201    /// // frees the memory associated with the batch. (allocated by llama.cpp)
202    /// drop(batch);
203    /// # Ok(())
204    /// # }
205    fn drop(&mut self) {
206        unsafe {
207            if self.allocated > 0 {
208                llama_batch_free(self.llama_batch);
209            }
210        }
211    }
212}