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}