Skip to main content

llama_cpp_4/
llama_batch.rs

1//! Safe wrapper around `llama_batch`.
2
3// Every `unsafe` block below is justified in a `SAFETY:` comment; keep it that
4// way. See the note on the lint in the workspace `Cargo.toml`.
5#![deny(clippy::undocumented_unsafe_blocks)]
6
7use crate::token::LlamaToken;
8use llama_cpp_sys_4::{llama_batch, llama_batch_free, llama_batch_init, llama_pos, llama_seq_id};
9
10/// A safe wrapper around `llama_batch`.
11#[derive(Debug)]
12#[allow(clippy::struct_field_names)]
13pub struct LlamaBatch {
14    /// 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
15    allocated: usize,
16    /// The logits that are initialized. Used by [`LlamaContext`] to ensure that only initialized logits are accessed.
17    pub(crate) initialized_logits: Vec<i32>,
18    /// The `llama_cpp` batch. always initialize by `llama_cpp_sys_4::llama_batch_init(allocated, <unknown>, <unknown>)`
19    pub(crate) llama_batch: llama_batch,
20}
21
22/// Errors that can occur when adding a token to a batch.
23#[derive(thiserror::Error, Debug, PartialEq, Eq)]
24pub enum BatchAddError {
25    /// There was not enough space in the batch to add the token.
26    #[error("Insufficient Space of {0}")]
27    InsufficientSpace(usize),
28}
29
30impl LlamaBatch {
31    /// Clear the batch. This does not free the memory associated with the batch, but it does reset
32    /// the number of tokens to 0.
33    pub fn clear(&mut self) {
34        self.llama_batch.n_tokens = 0;
35        self.initialized_logits.clear();
36    }
37
38    /// add a token to the batch for sequences `seq_ids` at position `pos`. If `logits` is true, the
39    /// token will be initialized and can be read from after the next decode.
40    ///
41    /// # Panics
42    ///
43    /// - `self.llama_batch.n_tokens` does not fit into a usize
44    /// - `seq_ids.len()` does not fit into a [`llama_seq_id`]
45    ///
46    /// # Errors
47    ///
48    /// returns a error if there is insufficient space in the buffer
49    pub fn add(
50        &mut self,
51        LlamaToken(id): LlamaToken,
52        pos: llama_pos,
53        seq_ids: &[i32],
54        logits: bool,
55    ) -> Result<(), BatchAddError> {
56        if self.allocated
57            < usize::try_from(self.n_tokens() + 1).expect("cannot fit n_tokens into a usize")
58        {
59            return Err(BatchAddError::InsufficientSpace(self.allocated));
60        }
61        let offset = self.llama_batch.n_tokens;
62        let offset_usize = usize::try_from(offset).expect("cannot fit n_tokens into a usize");
63        // SAFETY: every write below is at index `offset_usize`, which the
64        // capacity check above proved is `< self.allocated`. `llama_batch_init`
65        // allocated `allocated` elements for `token`, `pos`, `n_seq_id`,
66        // `seq_id` and `logits`, so each pointer is valid and aligned for a
67        // write at that index. `seq_id[offset]` is its own allocation of
68        // `n_seq_max` elements; `seq_ids.len()` is bounded by the caller, which
69        // is the one invariant this type does *not* check — see the note below.
70        unsafe {
71            // batch.token   [batch.n_tokens] = id;
72            self.llama_batch.token.add(offset_usize).write(id);
73            // batch.pos     [batch.n_tokens] = pos,
74            self.llama_batch.pos.add(offset_usize).write(pos);
75            // batch.n_seq_id[batch.n_tokens] = seq_ids.size();
76            self.llama_batch.n_seq_id.add(offset_usize).write(
77                llama_seq_id::try_from(seq_ids.len())
78                    .expect("cannot fit seq_ids.len() into a llama_seq_id"),
79            );
80            // for (size_t i = 0; i < seq_ids.size(); ++i) {
81            //     batch.seq_id[batch.n_tokens][i] = seq_ids[i];
82            // }
83            // NOTE: `llama_batch_init` sized each `seq_id[i]` row to the
84            // `n_seq_max` passed to `LlamaBatch::new`. Passing more than that
85            // many ids here writes past the row — the one unchecked precondition
86            // left in this type.
87            for (i, seq_id) in seq_ids.iter().enumerate() {
88                let tmp = *self.llama_batch.seq_id.add(offset_usize);
89                tmp.add(i).write(*seq_id);
90            }
91            // batch.logits  [batch.n_tokens] = logits;
92            self.llama_batch
93                .logits
94                .add(offset_usize)
95                .write(i8::from(logits));
96        }
97
98        if logits {
99            self.initialized_logits.push(offset);
100        } else {
101            self.initialized_logits.retain(|l| l != &offset);
102        }
103
104        // batch.n_tokens++;
105        self.llama_batch.n_tokens += 1;
106
107        Ok(())
108    }
109
110    /// Add a sequence of tokens to the batch for the given sequence id. If `logits_all` is true, the
111    /// tokens will be initialized and can be read from after the next decode.
112    ///
113    /// Either way the last token in the sequence will have its logits set to `true`.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if there is insufficient space in the buffer
118    ///
119    /// # Panics
120    ///
121    /// - `self.llama_batch.n_tokens` does not fit into a [`usize`]
122    /// - [`n_tokens - 1`] does not fit into a [`llama_pos`]
123    pub fn add_sequence(
124        &mut self,
125        tokens: &[LlamaToken],
126        seq_id: i32,
127        logits_all: bool,
128    ) -> Result<(), BatchAddError> {
129        let n_tokens_0 =
130            usize::try_from(self.llama_batch.n_tokens).expect("cannot fit n_tokens into a usize");
131        let n_tokens = tokens.len();
132
133        if self.allocated < n_tokens_0 + n_tokens {
134            return Err(BatchAddError::InsufficientSpace(self.allocated));
135        }
136
137        let last_index = llama_pos::try_from(n_tokens.saturating_sub(1))
138            .expect("cannot fit n_tokens into a llama_pos");
139        for (i, token) in (0..).zip(tokens.iter()) {
140            self.add(*token, i, &[seq_id], logits_all || i == last_index)?;
141        }
142
143        Ok(())
144    }
145
146    /// Create a new `LlamaBatch` that can contain up to `n_tokens` tokens.
147    ///
148    /// # Arguments
149    ///
150    /// - `n_tokens`: the maximum number of tokens that can be added to the batch
151    /// - `n_seq_max`: the maximum number of sequences that can be added to the batch (generally 1 unless you know what you are doing)
152    ///
153    /// # Panics
154    ///
155    /// Panics if `n_tokens` is greater than `i32::MAX`.
156    #[must_use]
157    pub fn new(n_tokens: usize, n_seq_max: i32) -> Self {
158        let n_tokens_i32 = i32::try_from(n_tokens).expect("cannot fit n_tokens into a i32");
159        // SAFETY: `llama_batch_init` only reads its three scalar arguments and
160        // returns an owned batch; `n_tokens_i32` was just range-checked. The
161        // `0` means "no embeddings", which is why `llama_batch.embd` stays null
162        // for the whole life of this type.
163        let batch = unsafe { llama_batch_init(n_tokens_i32, 0, n_seq_max) };
164
165        LlamaBatch {
166            allocated: n_tokens,
167            initialized_logits: vec![],
168            llama_batch: batch,
169        }
170    }
171
172    /// Returns the number of tokens in the batch.
173    #[must_use]
174    pub fn n_tokens(&self) -> i32 {
175        self.llama_batch.n_tokens
176    }
177
178    /// Create a batch from a slice of tokens for simple one-shot decoding.
179    ///
180    /// The returned batch uses the provided token buffer directly and does not own the memory.
181    /// All tokens are assigned to sequence 0 and logits are enabled for the last token only.
182    ///
183    /// **Note:** The returned batch does NOT free memory on drop — it borrows from the input
184    /// slice. The caller must ensure `tokens` outlives the returned batch.
185    ///
186    /// # Panics
187    ///
188    /// Panics if `tokens.len()` does not fit into an `i32`.
189    #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
190    #[must_use]
191    pub fn get_one(tokens: &mut [LlamaToken]) -> llama_batch {
192        // SAFETY: `LlamaToken` is `#[repr(transparent)]` over `llama_token`, so
193        // the cast is a no-op in layout, and the slice gives a valid pointer
194        // for `tokens.len()` elements. The returned batch *borrows* that
195        // pointer — which is why this returns the raw `llama_batch` rather than
196        // a `LlamaBatch`: there is no owned allocation to free, and the caller
197        // carries the obligation documented above to keep `tokens` alive.
198        unsafe {
199            llama_cpp_sys_4::llama_batch_get_one(tokens.as_mut_ptr().cast(), tokens.len() as i32)
200        }
201    }
202}
203
204impl Drop for LlamaBatch {
205    /// Drops the `LlamaBatch`.
206    ///
207    /// ```
208    /// # use llama_cpp_4::llama_batch::LlamaBatch;
209    /// # use std::error::Error;
210    /// # fn main() -> Result<(), Box<dyn Error>> {
211    /// let batch = LlamaBatch::new(512, 1);
212    /// // frees the memory associated with the batch. (allocated by llama.cpp)
213    /// drop(batch);
214    /// # Ok(())
215    /// # }
216    fn drop(&mut self) {
217        // SAFETY: `self.llama_batch` came from `llama_batch_init` in
218        // `LlamaBatch::new` and is freed exactly once, here. Nothing else in
219        // this type replaces it, and `LlamaBatch` is not `Copy`/`Clone`, so
220        // there is no second owner.
221        unsafe {
222            llama_batch_free(self.llama_batch);
223        }
224    }
225}