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 corresponds to the `n_embd` parameter of the context's model.
144    ///
145    /// # Errors
146    ///
147    /// - When the current context was constructed without enabling embeddings.
148    /// - If the current model had a pooling type of [`llama_cpp_sys_2::LLAMA_POOLING_TYPE_NONE`]
149    /// - If the given sequence index exceeds the max sequence id.
150    ///
151    /// # Panics
152    ///
153    /// * `n_embd` does not fit into a usize
154    pub fn embeddings_seq_ith(&self, i: i32) -> Result<&[f32], EmbeddingsError> {
155        if !self.embeddings_enabled {
156            return Err(EmbeddingsError::NotEnabled);
157        }
158
159        let n_embd =
160            usize::try_from(self.model.n_embd()).expect("n_embd does not fit into a usize");
161
162        unsafe {
163            let embedding = llama_cpp_sys_2::llama_get_embeddings_seq(self.context.as_ptr(), i);
164
165            // Technically also possible whenever `i >= max(batch.n_seq)`, but can't check that here.
166            if embedding.is_null() {
167                Err(EmbeddingsError::NonePoolType)
168            } else {
169                Ok(slice::from_raw_parts(embedding, n_embd))
170            }
171        }
172    }
173
174    /// Get the embeddings for the `i`th token in the current context.
175    ///
176    /// # Returns
177    ///
178    /// A slice containing the embeddings for the last decoded batch of the given token.
179    /// The size corresponds to the `n_embd` parameter of the context's model.
180    ///
181    /// # Errors
182    ///
183    /// - When the current context was constructed without enabling embeddings.
184    /// - When the given token didn't have logits enabled when it was passed.
185    /// - If the given token index exceeds the max token id.
186    ///
187    /// # Panics
188    ///
189    /// * `n_embd` does not fit into a usize
190    pub fn embeddings_ith(&self, i: i32) -> Result<&[f32], EmbeddingsError> {
191        if !self.embeddings_enabled {
192            return Err(EmbeddingsError::NotEnabled);
193        }
194
195        let n_embd =
196            usize::try_from(self.model.n_embd()).expect("n_embd does not fit into a usize");
197
198        unsafe {
199            let embedding = llama_cpp_sys_2::llama_get_embeddings_ith(self.context.as_ptr(), i);
200            // Technically also possible whenever `i >= batch.n_tokens`, but no good way of checking `n_tokens` here.
201            if embedding.is_null() {
202                Err(EmbeddingsError::LogitsNotEnabled)
203            } else {
204                Ok(slice::from_raw_parts(embedding, n_embd))
205            }
206        }
207    }
208
209    /// Get the logits for the last token in the context.
210    ///
211    /// # Returns
212    /// An iterator over unsorted `LlamaTokenData` containing the
213    /// logits for the last token in the context.
214    ///
215    /// # Panics
216    ///
217    /// - underlying logits data is null
218    pub fn candidates(&self) -> impl Iterator<Item = LlamaTokenData> + '_ {
219        (0_i32..).zip(self.get_logits()).map(|(i, logit)| {
220            let token = LlamaToken::new(i);
221            LlamaTokenData::new(token, *logit, 0_f32)
222        })
223    }
224
225    /// Get the token data array for the last token in the context.
226    ///
227    /// This is a convience method that implements:
228    /// ```ignore
229    /// LlamaTokenDataArray::from_iter(ctx.candidates(), false)
230    /// ```
231    ///
232    /// # Panics
233    ///
234    /// - underlying logits data is null
235    #[must_use]
236    pub fn token_data_array(&self) -> LlamaTokenDataArray {
237        LlamaTokenDataArray::from_iter(self.candidates(), false)
238    }
239
240    /// Token logits obtained from the last call to `decode()`.
241    /// The logits for which `batch.logits[i] != 0` are stored contiguously
242    /// in the order they have appeared in the batch.
243    /// Rows: number of tokens for which `batch.logits[i] != 0`
244    /// Cols: `n_vocab`
245    ///
246    /// # Returns
247    ///
248    /// A slice containing the logits for the last decoded token.
249    /// The size corresponds to the `n_vocab` parameter of the context's model.
250    ///
251    /// # Panics
252    ///
253    /// - `n_vocab` does not fit into a usize
254    /// - token data returned is null
255    #[must_use]
256    pub fn get_logits(&self) -> &[f32] {
257        let data = unsafe { llama_cpp_sys_2::llama_get_logits(self.context.as_ptr()) };
258        assert!(!data.is_null(), "logits data for last token is null");
259        let len = usize::try_from(self.model.n_vocab()).expect("n_vocab does not fit into a usize");
260
261        unsafe { slice::from_raw_parts(data, len) }
262    }
263
264    /// Get the logits for the ith token in the context.
265    ///
266    /// # Panics
267    ///
268    /// - logit `i` is not initialized.
269    pub fn candidates_ith(&self, i: i32) -> impl Iterator<Item = LlamaTokenData> + '_ {
270        (0_i32..).zip(self.get_logits_ith(i)).map(|(i, logit)| {
271            let token = LlamaToken::new(i);
272            LlamaTokenData::new(token, *logit, 0_f32)
273        })
274    }
275
276    /// Get the token data array for the ith token in the context.
277    ///
278    /// This is a convience method that implements:
279    /// ```ignore
280    /// LlamaTokenDataArray::from_iter(ctx.candidates_ith(i), false)
281    /// ```
282    ///
283    /// # Panics
284    ///
285    /// - logit `i` is not initialized.
286    #[must_use]
287    pub fn token_data_array_ith(&self, i: i32) -> LlamaTokenDataArray {
288        LlamaTokenDataArray::from_iter(self.candidates_ith(i), false)
289    }
290
291    /// Get the logits for the ith token in the context.
292    ///
293    /// # Panics
294    ///
295    /// - `i` is greater than `n_ctx`
296    /// - `n_vocab` does not fit into a usize
297    /// - logit `i` is not initialized.
298    #[must_use]
299    pub fn get_logits_ith(&self, i: i32) -> &[f32] {
300        assert!(
301            self.initialized_logits.contains(&i),
302            "logit {i} is not initialized. only {:?} is",
303            self.initialized_logits
304        );
305        assert!(
306            self.n_ctx() > u32::try_from(i).expect("i does not fit into a u32"),
307            "n_ctx ({}) must be greater than i ({})",
308            self.n_ctx(),
309            i
310        );
311
312        let data = unsafe { llama_cpp_sys_2::llama_get_logits_ith(self.context.as_ptr(), i) };
313        let len = usize::try_from(self.model.n_vocab()).expect("n_vocab does not fit into a usize");
314
315        unsafe { slice::from_raw_parts(data, len) }
316    }
317
318    /// Reset the timings for the context.
319    pub fn reset_timings(&mut self) {
320        unsafe { llama_cpp_sys_2::llama_perf_context_reset(self.context.as_ptr()) }
321    }
322
323    /// Returns the timings for the context.
324    pub fn timings(&mut self) -> LlamaTimings {
325        let timings = unsafe { llama_cpp_sys_2::llama_perf_context(self.context.as_ptr()) };
326        LlamaTimings { timings }
327    }
328
329    /// Sets a lora adapter.
330    ///
331    /// # Errors
332    ///
333    /// See [`LlamaLoraAdapterSetError`] for more information.
334    pub fn lora_adapter_set(
335        &self,
336        adapter: &mut LlamaLoraAdapter,
337        scale: f32,
338    ) -> Result<(), LlamaLoraAdapterSetError> {
339        let mut adapters = [adapter.lora_adapter.as_ptr()];
340        let mut scales = [scale];
341        let err_code = unsafe {
342            llama_cpp_sys_2::llama_set_adapters_lora(
343                self.context.as_ptr(),
344                adapters.as_mut_ptr(),
345                1,
346                scales.as_mut_ptr(),
347            )
348        };
349        if err_code != 0 {
350            return Err(LlamaLoraAdapterSetError::ErrorResult(err_code));
351        }
352
353        tracing::debug!("Set lora adapter");
354        Ok(())
355    }
356
357    /// Remove all lora adapters.
358    ///
359    /// Note: The upstream API now replaces all adapters at once via
360    /// `llama_set_adapters_lora`. This clears all adapters from the context.
361    ///
362    /// # Errors
363    ///
364    /// See [`LlamaLoraAdapterRemoveError`] for more information.
365    pub fn lora_adapter_remove(
366        &self,
367        _adapter: &mut LlamaLoraAdapter,
368    ) -> Result<(), LlamaLoraAdapterRemoveError> {
369        let err_code = unsafe {
370            llama_cpp_sys_2::llama_set_adapters_lora(
371                self.context.as_ptr(),
372                std::ptr::null_mut(),
373                0,
374                std::ptr::null_mut(),
375            )
376        };
377        if err_code != 0 {
378            return Err(LlamaLoraAdapterRemoveError::ErrorResult(err_code));
379        }
380
381        tracing::debug!("Remove lora adapter");
382        Ok(())
383    }
384
385    /// Get the backend-sampled token at the given index.
386    ///
387    /// This is part of the experimental backend sampling API. Only usable
388    /// when the context was created with at least one `llama_sampler_seq_config`.
389    ///
390    /// Returns `None` if no token was sampled at the given index
391    /// (i.e. the C API returned `LLAMA_TOKEN_NULL`).
392    ///
393    /// # Arguments
394    ///
395    /// * `i` - The token index, matching the order from the batch.
396    #[must_use]
397    pub fn sampled_token_ith(&self, i: i32) -> Option<LlamaToken> {
398        let token =
399            unsafe { llama_cpp_sys_2::llama_get_sampled_token_ith(self.context.as_ptr(), i) };
400        // LLAMA_TOKEN_NULL is #define'd as -1 in llama.h (not exposed by bindgen)
401        if token == -1 {
402            None
403        } else {
404            Some(LlamaToken(token))
405        }
406    }
407
408    /// Print a breakdown of per-device memory use to the default logger.
409    #[cfg(feature = "common")]
410    pub fn print_memory_breakdown(&self) {
411        unsafe { llama_cpp_sys_2::llama_rs_memory_breakdown_print(self.context.as_ptr()) }
412    }
413}
414
415impl Drop for LlamaContext<'_> {
416    fn drop(&mut self) {
417        unsafe { llama_cpp_sys_2::llama_free(self.context.as_ptr()) }
418    }
419}