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