Skip to main content

llama_cpp_2/
context.rs

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