Skip to main content

llama_cpp_2/
context.rs

1//! Safe wrapper around `llama_context`.
2
3use std::fmt::{Debug, Formatter};
4use std::num::NonZeroI32;
5use std::ptr::NonNull;
6use std::slice;
7
8use crate::llama_batch::LlamaBatch;
9use crate::model::{LlamaLoraAdapter, LlamaModel};
10use crate::sampling::LlamaSampler;
11use crate::timing::LlamaTimings;
12use crate::token::data::LlamaTokenData;
13use crate::token::data_array::LlamaTokenDataArray;
14use crate::token::LlamaToken;
15use crate::{
16    DecodeError, EmbeddingsError, EncodeError, LlamaLoraAdapterRemoveError,
17    LlamaLoraAdapterSetError,
18};
19
20pub mod kv_cache;
21pub mod params;
22pub mod session;
23
24/// Safe wrapper around `llama_context`.
25#[allow(clippy::module_name_repetitions)]
26pub struct LlamaContext<'a> {
27    pub(crate) context: NonNull<llama_cpp_sys_2::llama_context>,
28    /// a reference to the contexts model.
29    pub model: &'a LlamaModel,
30    initialized_logits: Vec<i32>,
31    embeddings_enabled: bool,
32    /// Backend samplers kept alive for the context's lifetime.
33    _backend_samplers: Vec<(i32, LlamaSampler)>,
34}
35
36impl Debug for LlamaContext<'_> {
37    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38        f.debug_struct("LlamaContext")
39            .field("context", &self.context)
40            .finish()
41    }
42}
43
44impl<'model> LlamaContext<'model> {
45    pub(crate) fn new(
46        llama_model: &'model LlamaModel,
47        llama_context: NonNull<llama_cpp_sys_2::llama_context>,
48        embeddings_enabled: bool,
49    ) -> Self {
50        Self {
51            context: llama_context,
52            model: llama_model,
53            initialized_logits: Vec::new(),
54            embeddings_enabled,
55            _backend_samplers: Vec::new(),
56        }
57    }
58
59    pub(crate) fn with_samplers(
60        llama_model: &'model LlamaModel,
61        llama_context: NonNull<llama_cpp_sys_2::llama_context>,
62        embeddings_enabled: bool,
63        backend_samplers: Vec<(i32, LlamaSampler)>,
64    ) -> Self {
65        Self {
66            context: llama_context,
67            model: llama_model,
68            initialized_logits: Vec::new(),
69            embeddings_enabled,
70            _backend_samplers: backend_samplers,
71        }
72    }
73
74    /// Gets the max number of logical tokens that can be submitted to decode. Must be greater than or equal to [`Self::n_ubatch`].
75    #[must_use]
76    pub fn n_batch(&self) -> u32 {
77        unsafe { llama_cpp_sys_2::llama_n_batch(self.context.as_ptr()) }
78    }
79
80    /// Gets the max number of physical tokens (hardware level) to decode in batch. Must be less than or equal to [`Self::n_batch`].
81    #[must_use]
82    pub fn n_ubatch(&self) -> u32 {
83        unsafe { llama_cpp_sys_2::llama_n_ubatch(self.context.as_ptr()) }
84    }
85
86    /// Gets the size of the context.
87    #[must_use]
88    pub fn n_ctx(&self) -> u32 {
89        unsafe { llama_cpp_sys_2::llama_n_ctx(self.context.as_ptr()) }
90    }
91
92    /// Decodes the batch.
93    ///
94    /// # Errors
95    ///
96    /// - `DecodeError` if the decoding failed.
97    ///
98    /// # Panics
99    ///
100    /// - the returned [`std::ffi::c_int`] from llama-cpp does not fit into a i32 (this should never happen on most systems)
101    pub fn decode(&mut self, batch: &mut LlamaBatch) -> Result<(), DecodeError> {
102        let result =
103            unsafe { llama_cpp_sys_2::llama_decode(self.context.as_ptr(), batch.llama_batch) };
104
105        match NonZeroI32::new(result) {
106            None => {
107                self.initialized_logits
108                    .clone_from(&batch.initialized_logits);
109                Ok(())
110            }
111            Some(error) => Err(DecodeError::from(error)),
112        }
113    }
114
115    /// Encodes the batch.
116    ///
117    /// # Errors
118    ///
119    /// - `EncodeError` if the decoding failed.
120    ///
121    /// # Panics
122    ///
123    /// - the returned [`std::ffi::c_int`] from llama-cpp does not fit into a i32 (this should never happen on most systems)
124    pub fn encode(&mut self, batch: &mut LlamaBatch) -> Result<(), EncodeError> {
125        let result =
126            unsafe { llama_cpp_sys_2::llama_encode(self.context.as_ptr(), batch.llama_batch) };
127
128        match NonZeroI32::new(result) {
129            None => {
130                self.initialized_logits
131                    .clone_from(&batch.initialized_logits);
132                Ok(())
133            }
134            Some(error) => Err(EncodeError::from(error)),
135        }
136    }
137
138    /// Get the embeddings for the `i`th sequence in the current context.
139    ///
140    /// # Returns
141    ///
142    /// A slice containing the embeddings for the last decoded batch.
143    /// The size is the pooling-derived output width: `n_cls_out` for RANK,
144    /// `n_embd_out` otherwise — NOT `n_embd` (llama.h:1029 /
145    /// llama-context.cpp's extraction switch).
146    ///
147    /// # Errors
148    ///
149    /// - When the current context was constructed without enabling embeddings.
150    /// - If the current model had a pooling type of [`llama_cpp_sys_2::LLAMA_POOLING_TYPE_NONE`]
151    /// - If the given sequence index exceeds the max sequence id.
152    ///
153    /// # Panics
154    ///
155    /// * `n_embd` does not fit into a usize
156    pub fn embeddings_seq_ith(&self, i: i32) -> Result<&[f32], EmbeddingsError> {
157        if !self.embeddings_enabled {
158            return Err(EmbeddingsError::NotEnabled);
159        }
160
161        unsafe {
162            let embedding = llama_cpp_sys_2::llama_get_embeddings_seq(self.context.as_ptr(), i);
163
164            // Technically also possible whenever `i >= max(batch.n_seq)`, but can't check that here.
165            if embedding.is_null() {
166                Err(EmbeddingsError::NonePoolType)
167            } else {
168                Ok(slice::from_raw_parts(embedding, self.embeddings_out_len()))
169            }
170        }
171    }
172
173    /// Get the embeddings for the `i`th token in the current context.
174    ///
175    /// # Returns
176    ///
177    /// A slice containing the embeddings for the last decoded batch of the given token.
178    /// The size is the pooling-derived output width: `n_cls_out` for RANK,
179    /// `n_embd_out` otherwise — NOT `n_embd` (llama.h:1029 /
180    /// llama-context.cpp's extraction switch).
181    ///
182    /// # Errors
183    ///
184    /// - When the current context was constructed without enabling embeddings.
185    /// - When the given token didn't have logits enabled when it was passed.
186    /// - If the given token index exceeds the max token id.
187    ///
188    /// # Panics
189    ///
190    /// * `n_embd` does not fit into a usize
191    pub fn embeddings_ith(&self, i: i32) -> Result<&[f32], EmbeddingsError> {
192        if !self.embeddings_enabled {
193            return Err(EmbeddingsError::NotEnabled);
194        }
195
196        unsafe {
197            let embedding = llama_cpp_sys_2::llama_get_embeddings_ith(self.context.as_ptr(), i);
198            // Technically also possible whenever `i >= batch.n_tokens`, but no good way of checking `n_tokens` here.
199            if embedding.is_null() {
200                Err(EmbeddingsError::LogitsNotEnabled)
201            } else {
202                Ok(slice::from_raw_parts(embedding, self.embeddings_out_len()))
203            }
204        }
205    }
206
207    /// The correct output width for an embeddings read, keyed on the context's
208    /// LIVE pooling type rather than the model's `n_embd`.
209    ///
210    /// RANK reads return `float[n_cls_out]` (default 1) per llama.h:1029; every
211    /// other pooling mode extracts at `n_embd_out` per llama-context.cpp's
212    /// extraction switch (which diverges from `n_embd` whenever
213    /// `{arch}.embedding_length_out` is present).
214    fn embeddings_out_len(&self) -> usize {
215        let pooling = unsafe { llama_cpp_sys_2::llama_pooling_type(self.context.as_ptr()) };
216        if pooling == llama_cpp_sys_2::LLAMA_POOLING_TYPE_RANK {
217            usize::try_from(self.model.n_cls_out()).expect("n_cls_out does not fit into a usize")
218        } else {
219            usize::try_from(self.model.n_embd_out()).expect("n_embd_out does not fit into a usize")
220        }
221    }
222
223    /// Get the logits for the last token in the context.
224    ///
225    /// # Returns
226    /// An iterator over unsorted `LlamaTokenData` containing the
227    /// logits for the last token in the context.
228    ///
229    /// # Panics
230    ///
231    /// - underlying logits data is null
232    pub fn candidates(&self) -> impl Iterator<Item = LlamaTokenData> + '_ {
233        (0_i32..).zip(self.get_logits()).map(|(i, logit)| {
234            let token = LlamaToken::new(i);
235            LlamaTokenData::new(token, *logit, 0_f32)
236        })
237    }
238
239    /// Get the token data array for the last token in the context.
240    ///
241    /// This is a convience method that implements:
242    /// ```ignore
243    /// LlamaTokenDataArray::from_iter(ctx.candidates(), false)
244    /// ```
245    ///
246    /// # Panics
247    ///
248    /// - underlying logits data is null
249    #[must_use]
250    pub fn token_data_array(&self) -> LlamaTokenDataArray {
251        LlamaTokenDataArray::from_iter(self.candidates(), false)
252    }
253
254    /// Token logits obtained from the last call to `decode()`.
255    /// The logits for which `batch.logits[i] != 0` are stored contiguously
256    /// in the order they have appeared in the batch.
257    /// Rows: number of tokens for which `batch.logits[i] != 0`
258    /// Cols: `n_vocab`
259    ///
260    /// # Returns
261    ///
262    /// A slice containing the logits for the last decoded token.
263    /// The size corresponds to the `n_vocab` parameter of the context's model.
264    ///
265    /// # Panics
266    ///
267    /// - `n_vocab` does not fit into a usize
268    /// - token data returned is null
269    #[must_use]
270    pub fn get_logits(&self) -> &[f32] {
271        let data = unsafe { llama_cpp_sys_2::llama_get_logits(self.context.as_ptr()) };
272        assert!(!data.is_null(), "logits data for last token is null");
273        let len = usize::try_from(self.model.n_vocab()).expect("n_vocab does not fit into a usize");
274
275        unsafe { slice::from_raw_parts(data, len) }
276    }
277
278    /// Get the logits for the ith token in the context.
279    ///
280    /// # Panics
281    ///
282    /// - logit `i` is not initialized.
283    pub fn candidates_ith(&self, i: i32) -> impl Iterator<Item = LlamaTokenData> + '_ {
284        (0_i32..).zip(self.get_logits_ith(i)).map(|(i, logit)| {
285            let token = LlamaToken::new(i);
286            LlamaTokenData::new(token, *logit, 0_f32)
287        })
288    }
289
290    /// Get the token data array for the ith token in the context.
291    ///
292    /// This is a convience method that implements:
293    /// ```ignore
294    /// LlamaTokenDataArray::from_iter(ctx.candidates_ith(i), false)
295    /// ```
296    ///
297    /// # Panics
298    ///
299    /// - logit `i` is not initialized.
300    #[must_use]
301    pub fn token_data_array_ith(&self, i: i32) -> LlamaTokenDataArray {
302        LlamaTokenDataArray::from_iter(self.candidates_ith(i), false)
303    }
304
305    /// Get the logits for the ith token in the context.
306    ///
307    /// # Panics
308    ///
309    /// - `i` is greater than `n_ctx`
310    /// - `n_vocab` does not fit into a usize
311    /// - logit `i` is not initialized.
312    #[must_use]
313    pub fn get_logits_ith(&self, i: i32) -> &[f32] {
314        assert!(
315            self.initialized_logits.contains(&i),
316            "logit {i} is not initialized. only {:?} is",
317            self.initialized_logits
318        );
319        assert!(
320            self.n_ctx() > u32::try_from(i).expect("i does not fit into a u32"),
321            "n_ctx ({}) must be greater than i ({})",
322            self.n_ctx(),
323            i
324        );
325
326        let data = unsafe { llama_cpp_sys_2::llama_get_logits_ith(self.context.as_ptr(), i) };
327        let len = usize::try_from(self.model.n_vocab()).expect("n_vocab does not fit into a usize");
328
329        unsafe { slice::from_raw_parts(data, len) }
330    }
331
332    /// Reset the timings for the context.
333    pub fn reset_timings(&mut self) {
334        unsafe { llama_cpp_sys_2::llama_perf_context_reset(self.context.as_ptr()) }
335    }
336
337    /// Returns the timings for the context.
338    pub fn timings(&mut self) -> LlamaTimings {
339        let timings = unsafe { llama_cpp_sys_2::llama_perf_context(self.context.as_ptr()) };
340        LlamaTimings { timings }
341    }
342
343    /// Sets a lora adapter.
344    ///
345    /// # Errors
346    ///
347    /// See [`LlamaLoraAdapterSetError`] for more information.
348    pub fn lora_adapter_set(
349        &self,
350        adapter: &mut LlamaLoraAdapter,
351        scale: f32,
352    ) -> Result<(), LlamaLoraAdapterSetError> {
353        let mut adapters = [adapter.lora_adapter.as_ptr()];
354        let mut scales = [scale];
355        let err_code = unsafe {
356            llama_cpp_sys_2::llama_set_adapters_lora(
357                self.context.as_ptr(),
358                adapters.as_mut_ptr(),
359                1,
360                scales.as_mut_ptr(),
361            )
362        };
363        if err_code != 0 {
364            return Err(LlamaLoraAdapterSetError::ErrorResult(err_code));
365        }
366
367        tracing::debug!("Set lora adapter");
368        Ok(())
369    }
370
371    /// Remove all lora adapters.
372    ///
373    /// Note: The upstream API now replaces all adapters at once via
374    /// `llama_set_adapters_lora`. This clears all adapters from the context.
375    ///
376    /// # Errors
377    ///
378    /// See [`LlamaLoraAdapterRemoveError`] for more information.
379    pub fn lora_adapter_remove(
380        &self,
381        _adapter: &mut LlamaLoraAdapter,
382    ) -> Result<(), LlamaLoraAdapterRemoveError> {
383        let err_code = unsafe {
384            llama_cpp_sys_2::llama_set_adapters_lora(
385                self.context.as_ptr(),
386                std::ptr::null_mut(),
387                0,
388                std::ptr::null_mut(),
389            )
390        };
391        if err_code != 0 {
392            return Err(LlamaLoraAdapterRemoveError::ErrorResult(err_code));
393        }
394
395        tracing::debug!("Remove lora adapter");
396        Ok(())
397    }
398
399    /// Get the backend-sampled token at the given index.
400    ///
401    /// This is part of the experimental backend sampling API. Only usable
402    /// when the context was created with at least one `llama_sampler_seq_config`.
403    ///
404    /// Returns `None` if no token was sampled at the given index
405    /// (i.e. the C API returned `LLAMA_TOKEN_NULL`).
406    ///
407    /// # Arguments
408    ///
409    /// * `i` - The token index, matching the order from the batch.
410    #[must_use]
411    pub fn sampled_token_ith(&self, i: i32) -> Option<LlamaToken> {
412        let token =
413            unsafe { llama_cpp_sys_2::llama_get_sampled_token_ith(self.context.as_ptr(), i) };
414        // LLAMA_TOKEN_NULL is #define'd as -1 in llama.h (not exposed by bindgen)
415        if token == -1 {
416            None
417        } else {
418            Some(LlamaToken(token))
419        }
420    }
421
422    /// Print a breakdown of per-device memory use to the default logger.
423    #[cfg(feature = "common")]
424    pub fn print_memory_breakdown(&self) {
425        unsafe { llama_cpp_sys_2::llama_rs_memory_breakdown_print(self.context.as_ptr()) }
426    }
427}
428
429impl Drop for LlamaContext<'_> {
430    fn drop(&mut self) {
431        unsafe { llama_cpp_sys_2::llama_free(self.context.as_ptr()) }
432    }
433}