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}