Skip to main content

llama_cpp_2/
vocab.rs

1//! A safe wrapper around `llama_vocab`.
2
3use std::ffi::{c_char, CStr};
4use std::marker::PhantomData;
5use std::mem::MaybeUninit;
6use std::num::NonZeroU16;
7use std::ptr::NonNull;
8use std::slice;
9
10use crate::model::LlamaModel;
11use crate::token::LlamaToken;
12use crate::token_type::LlamaTokenAttrs;
13
14/// A safe wrapper around `llama_vocab`.
15///
16/// The vocabulary is owned by the [`LlamaModel`] it was retrieved from, so
17/// dropping a [`LlamaVocab`] does not free the underlying `llama_vocab`.
18#[derive(Debug)]
19#[repr(transparent)]
20#[allow(clippy::module_name_repetitions)]
21pub struct LlamaVocab<'model> {
22    vocab: NonNull<llama_cpp_sys_2::llama_vocab>,
23    _phantom: PhantomData<&'model LlamaModel>,
24}
25
26// SAFETY: The vocabulary is immutable.
27unsafe impl Send for LlamaVocab<'_> {}
28unsafe impl Sync for LlamaVocab<'_> {}
29
30impl<'model> LlamaVocab<'model> {
31    pub(crate) fn as_ptr(&self) -> *const llama_cpp_sys_2::llama_vocab {
32        self.vocab.as_ptr()
33    }
34
35    pub(crate) fn new(ptr: *const llama_cpp_sys_2::llama_vocab) -> Option<Self> {
36        Some(Self {
37            vocab: NonNull::new(ptr.cast_mut())?,
38            _phantom: PhantomData,
39        })
40    }
41
42    /// The type of the vocabulary.
43    #[must_use]
44    pub fn vocab_type(&self) -> VocabType {
45        let vocab_type = unsafe { llama_cpp_sys_2::llama_vocab_type(self.vocab.as_ptr()) };
46        VocabType::try_from(vocab_type).expect("invalid vocab type")
47    }
48
49    /// The number of tokens the model was trained on.
50    #[must_use]
51    pub fn n_tokens(&self) -> i32 {
52        unsafe { llama_cpp_sys_2::llama_vocab_n_tokens(self.vocab.as_ptr()) }
53    }
54
55    /// The text representation of a token, if any.
56    #[must_use]
57    pub fn text(&self, token: LlamaToken) -> Option<&CStr> {
58        let text = unsafe { llama_cpp_sys_2::llama_vocab_get_text(self.vocab.as_ptr(), token.0) };
59        if text.is_null() {
60            None
61        } else {
62            // SAFETY: The pointer is not NULL, and it is valid for as long as
63            // the `llama_vocab` is (it's stored in that).
64            Some(unsafe { CStr::from_ptr(text) })
65        }
66    }
67
68    /// The score of a token.
69    #[must_use]
70    pub fn score(&self, token: LlamaToken) -> f32 {
71        unsafe { llama_cpp_sys_2::llama_vocab_get_score(self.vocab.as_ptr(), token.0) }
72    }
73
74    /// The attributes of a token.
75    #[must_use]
76    pub fn attr(&self, token: LlamaToken) -> LlamaTokenAttrs {
77        let attrs = unsafe { llama_cpp_sys_2::llama_vocab_get_attr(self.vocab.as_ptr(), token.0) };
78        LlamaTokenAttrs::try_from(attrs).expect("token attr is valid")
79    }
80
81    /// Check if the token is supposed to end generation (end-of-generation, eg. EOS, EOT, etc.)
82    #[must_use]
83    pub fn is_eog(&self, token: LlamaToken) -> bool {
84        unsafe { llama_cpp_sys_2::llama_vocab_is_eog(self.vocab.as_ptr(), token.0) }
85    }
86
87    /// Identify if the token is a control token or a render-able token.
88    #[must_use]
89    pub fn is_control(&self, token: LlamaToken) -> bool {
90        unsafe { llama_cpp_sys_2::llama_vocab_is_control(self.vocab.as_ptr(), token.0) }
91    }
92
93    /// The beginning of stream token.
94    #[must_use]
95    pub fn bos(&self) -> LlamaToken {
96        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_bos(self.vocab.as_ptr()) })
97    }
98
99    /// The end of stream token.
100    #[must_use]
101    pub fn eos(&self) -> LlamaToken {
102        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_eos(self.vocab.as_ptr()) })
103    }
104
105    /// The end of turn token.
106    #[must_use]
107    pub fn eot(&self) -> LlamaToken {
108        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_eot(self.vocab.as_ptr()) })
109    }
110
111    /// The separator token.
112    #[must_use]
113    pub fn sep(&self) -> LlamaToken {
114        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_sep(self.vocab.as_ptr()) })
115    }
116
117    /// The newline token.
118    #[must_use]
119    pub fn nl(&self) -> LlamaToken {
120        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_nl(self.vocab.as_ptr()) })
121    }
122
123    /// The padding token.
124    #[must_use]
125    pub fn pad(&self) -> LlamaToken {
126        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_pad(self.vocab.as_ptr()) })
127    }
128
129    /// The mask token.
130    #[must_use]
131    pub fn mask(&self) -> LlamaToken {
132        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_mask(self.vocab.as_ptr()) })
133    }
134
135    /// Whether to add the beginning of stream token when tokenizing.
136    #[must_use]
137    pub fn should_add_bos(&self) -> bool {
138        unsafe { llama_cpp_sys_2::llama_vocab_get_add_bos(self.vocab.as_ptr()) }
139    }
140
141    /// Whether to add the end of stream token when tokenizing.
142    #[must_use]
143    pub fn should_add_eos(&self) -> bool {
144        unsafe { llama_cpp_sys_2::llama_vocab_get_add_eos(self.vocab.as_ptr()) }
145    }
146
147    /// Whether to add the separator token when tokenizing.
148    #[must_use]
149    pub fn should_add_sep(&self) -> bool {
150        unsafe { llama_cpp_sys_2::llama_vocab_get_add_sep(self.vocab.as_ptr()) }
151    }
152
153    /// Model-specific suppress tokens.
154    ///
155    /// (gguf key: `tokenizer.ggml.suppress_tokens`)
156    #[must_use]
157    pub fn suppress_tokens(&self) -> &[LlamaToken] {
158        let mut n_suppress_tokens = 0_i32;
159        let tokens = unsafe {
160            llama_cpp_sys_2::llama_vocab_get_suppress_tokens(
161                self.vocab.as_ptr(),
162                &mut n_suppress_tokens,
163            )
164        };
165        if tokens.is_null() || n_suppress_tokens == 0 {
166            return &[];
167        }
168        // `LlamaToken` is `#[repr(transparent)]` over `llama_token`.
169        let tokens = tokens.cast::<LlamaToken>();
170        unsafe { slice::from_raw_parts(tokens, n_suppress_tokens as usize) }
171    }
172
173    /// The fill-in-the-middle prefix token.
174    #[must_use]
175    pub fn fim_pre(&self) -> LlamaToken {
176        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_fim_pre(self.vocab.as_ptr()) })
177    }
178
179    /// The fill-in-the-middle suffix token.
180    #[must_use]
181    pub fn fim_suf(&self) -> LlamaToken {
182        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_fim_suf(self.vocab.as_ptr()) })
183    }
184
185    /// The fill-in-the-middle middle token.
186    #[must_use]
187    pub fn fim_mid(&self) -> LlamaToken {
188        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_fim_mid(self.vocab.as_ptr()) })
189    }
190
191    /// The fill-in-the-middle padding token.
192    #[must_use]
193    pub fn fim_pad(&self) -> LlamaToken {
194        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_fim_pad(self.vocab.as_ptr()) })
195    }
196
197    /// The fill-in-the-middle repo token.
198    #[must_use]
199    pub fn fim_rep(&self) -> LlamaToken {
200        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_fim_rep(self.vocab.as_ptr()) })
201    }
202
203    /// The fill-in-the-middle separator token.
204    #[must_use]
205    pub fn fim_sep(&self) -> LlamaToken {
206        LlamaToken(unsafe { llama_cpp_sys_2::llama_vocab_fim_sep(self.vocab.as_ptr()) })
207    }
208
209    /// Tokenize text to a [`Vec`] of tokens.
210    ///
211    /// This is a convenience wrapper on top of [`tokenize_into`][Self::tokenize_into].
212    ///
213    /// # Example
214    ///
215    /// Tokenize a string of text.
216    ///
217    /// ```no_run
218    /// # let vocab: llama_cpp_2::vocab::LlamaVocab<'_> = todo!();
219    /// let tokens = vocab.tokenize("Hello, World!".as_bytes(), true, true);
220    /// ```
221    pub fn tokenize(&self, text: &[u8], add_special: bool, parse_special: bool) -> Vec<LlamaToken> {
222        let tokens_estimation = std::cmp::max(8, (text.len() / 2) + usize::from(add_special));
223        let mut output = Vec::with_capacity(tokens_estimation);
224        self.tokenize_into(text, &mut output, add_special, parse_special);
225        output
226    }
227
228    /// Tokenize text into a user-provided buffer.
229    ///
230    /// The output is placed into the vector's spare capacity, and the `Vec`
231    /// will be resized if the output doesn't fit.
232    ///
233    /// Setting `add_special` allows this function to add BOS and EOS tokens
234    /// if the model is configured to do so.
235    ///
236    /// Setting `parse_special` allows this function to tokenize special
237    /// and/or control tokens which otherwise are not exposed and treated as
238    /// plaintext. Setting it does not insert a leading space.
239    pub fn tokenize_into(
240        &self,
241        text: &[u8],
242        buffer: &mut Vec<LlamaToken>,
243        add_special: bool,
244        parse_special: bool,
245    ) {
246        let result = self.tokenize_raw(
247            text,
248            buffer.spare_capacity_mut(),
249            add_special,
250            parse_special,
251        );
252
253        let size = match result {
254            // It's fine to panic here, having a context size that causes
255            // this large an output string is never going to happen.
256            i32::MIN => panic!("output string would've been larger than 2GB"),
257            result if result.is_negative() => {
258                // Cannot overflow, usize > i32, and `-written` is non-negative.
259                let required_size = (-result) as usize;
260
261                // If we fail the first time we resize the vector to the
262                // correct size and try again.
263                buffer.reserve_exact(required_size);
264
265                let result = self.tokenize_raw(
266                    text,
267                    buffer.spare_capacity_mut(),
268                    add_special,
269                    parse_special,
270                );
271
272                // Should succeed, we know the vector was correctly sized now.
273                debug_assert!(result.is_positive());
274
275                result as usize
276            }
277            // Cannot overflow, usize > i32
278            written => written as usize,
279        };
280
281        // SAFETY: llama.cpp has written `size` items into the spare capacity.
282        unsafe { buffer.set_len(buffer.len() + size) };
283    }
284
285    fn tokenize_raw(
286        &self,
287        text: &[u8],
288        buffer: &mut [MaybeUninit<LlamaToken>],
289        add_special: bool,
290        parse_special: bool,
291    ) -> i32 {
292        unsafe {
293            llama_cpp_sys_2::llama_tokenize(
294                self.vocab.as_ptr(),
295                text.as_ptr().cast::<c_char>(),
296                // Assume the text is short enough to fit in `llama_tokenize`.
297                // If not, the user tried to tokenize a >2GB large text, which
298                // is gonna fail horribly elsewhere anyhow.
299                text.len().try_into().expect("input text was too large"),
300                buffer.as_mut_ptr().cast::<llama_cpp_sys_2::llama_token>(),
301                // It's fine to clamp the buffer length here, we'll get an
302                // error from llama.cpp anyhow if there are too many tokens to
303                // tokenize.
304                buffer.len().try_into().unwrap_or(i32::MAX),
305                add_special,
306                parse_special,
307            )
308        }
309    }
310
311    /// Detokenize a list of tokens.
312    ///
313    /// This is a convenience wrapper on top of [`detokenize_into`][Self::detokenize_into].
314    pub fn detokenize(
315        &self,
316        tokens: &[LlamaToken],
317        remove_special: bool,
318        unparse_special: bool,
319    ) -> Vec<u8> {
320        let tokens_estimation = std::cmp::max(8, (tokens.len() * 2) - usize::from(remove_special));
321        let mut output = Vec::with_capacity(tokens_estimation);
322        self.detokenize_into(tokens, &mut output, remove_special, unparse_special);
323        output
324    }
325
326    /// Detokenize a list of tokens into a user-provided buffer.
327    ///
328    /// This is the inverse of [tokenization][Self::tokenize_into].
329    ///
330    /// The output is placed into the vector's spare capacity, and the `Vec`
331    /// will be resized if the output doesn't fit.
332    ///
333    /// Setting `remove_special` allows this function to remove BOS and EOS
334    /// tokens if the model is configured to do so.
335    ///
336    /// Special tokens are rendered in the output if `unparse_special` is set.
337    ///
338    /// # Example
339    ///
340    /// Convert a list of tokens to a string.
341    ///
342    /// When converting tokens while streaming, you probably want to use
343    /// [`token_to_piece`][Self::token_to_piece] instead.
344    ///
345    /// ```no_run
346    /// # let tokens: Vec<llama_cpp_2::token::LlamaToken> = todo!();
347    /// # let vocab: llama_cpp_2::vocab::LlamaVocab<'_> = todo!();
348    /// let token_bytes = vocab.detokenize(&tokens, true, true);
349    /// let (token_string, _had_errors) = encoding_rs::UTF_8.decode_without_bom_handling(&token_bytes);
350    /// print!("{token_string}");
351    /// ```
352    pub fn detokenize_into(
353        &self,
354        tokens: &[LlamaToken],
355        buffer: &mut Vec<u8>,
356        remove_special: bool,
357        unparse_special: bool,
358    ) {
359        let result = self.detokenize_raw(
360            tokens,
361            buffer.spare_capacity_mut(),
362            remove_special,
363            unparse_special,
364        );
365
366        let size = match result {
367            result if result.is_negative() => {
368                // Cannot overflow, usize > i32, and `-written` is non-negative.
369                let required_size = (-result) as usize;
370
371                // If we fail the first time we resize the vector to the
372                // correct size and try again.
373                buffer.reserve_exact(required_size);
374
375                let result = self.detokenize_raw(
376                    tokens,
377                    buffer.spare_capacity_mut(),
378                    remove_special,
379                    unparse_special,
380                );
381
382                // Should succeed, we know the vector was correctly sized now.
383                debug_assert!(result.is_positive());
384
385                result as usize
386            }
387            // Cannot overflow, usize > i32
388            written => written as usize,
389        };
390
391        // SAFETY: llama.cpp has written `size` items into the spare capacity.
392        unsafe { buffer.set_len(buffer.len() + size) };
393    }
394
395    fn detokenize_raw(
396        &self,
397        tokens: &[LlamaToken],
398        buffer: &mut [MaybeUninit<u8>],
399        remove_special: bool,
400        unparse_special: bool,
401    ) -> i32 {
402        unsafe {
403            llama_cpp_sys_2::llama_detokenize(
404                self.vocab.as_ptr(),
405                tokens.as_ptr().cast::<llama_cpp_sys_2::llama_token>(),
406                // Assume the tokens are few enough to fit in `llama_detokenize`.
407                // If not, the user tried to detokenize a >2GB large list of
408                // tokens, which is gonna fail horribly elsewhere anyhow.
409                tokens.len().try_into().expect("too many input tokens"),
410                buffer.as_mut_ptr().cast::<c_char>(),
411                // It's fine to clamp the buffer length here, we'll get an
412                // error from llama.cpp anyhow if there are too many tokens to
413                // detokenize.
414                buffer.len().try_into().unwrap_or(i32::MAX),
415                remove_special,
416                unparse_special,
417            )
418        }
419    }
420
421    /// Detokenize a single token.
422    ///
423    /// This is a convenience method on top of [`token_to_piece_into`][Self::token_to_piece_into].
424    pub fn token_to_piece(
425        &self,
426        token: LlamaToken,
427        special: bool,
428        lstrip: Option<NonZeroU16>,
429    ) -> Vec<u8> {
430        let mut buffer = Vec::with_capacity(8);
431        self.token_to_piece_into(token, &mut buffer, special, lstrip);
432        buffer
433    }
434
435    /// Detokenize a single token into a caller-provided buffer.
436    ///
437    /// The output is placed into the vector's spare capacity, and the `Vec`
438    /// will be resized if the output doesn't fit.
439    ///
440    /// Special tokens are rendered in the output if `special` is set.
441    ///
442    /// This will skip up to `lstrip` leading spaces before copying, which can
443    /// be useful when encoding/decoding multiple tokens with
444    /// 'add_space_prefix'.
445    ///
446    /// # Example
447    ///
448    /// Convert a stream of tokens to a string, with proper partial UTF-8
449    /// decoding (such as for emoji support) and reusing buffers.
450    ///
451    /// ```no_run
452    /// # let tokens: Vec<llama_cpp_2::token::LlamaToken> = todo!();
453    /// # let vocab: llama_cpp_2::vocab::LlamaVocab<'_> = todo!();
454    /// let mut buffer = Vec::new();
455    /// let mut decoder = encoding_rs::UTF_8.new_decoder();
456    /// let mut token_string = String::new();
457    /// for token in tokens {
458    ///     // Clear buffers.
459    ///     buffer.clear();
460    ///     token_string.clear();
461    ///
462    ///     // Get the token bytes.
463    ///     vocab.token_to_piece_into(token, &mut buffer, true, None);
464    ///
465    ///     // Ensure the size of the string buffer is large enough.
466    ///     let max_len = decoder.max_utf8_buffer_length(buffer.len()).unwrap();
467    ///     token_string.reserve(max_len);
468    ///
469    ///     // Grab the parts of the buffer that's valid (and leave the rest
470    ///     // for the next token to reuse).
471    ///     let (result, read, _) = decoder.decode_to_string(&buffer, &mut token_string, false);
472    ///     assert!(
473    ///         matches!(result, encoding_rs::CoderResult::InputEmpty) && read == buffer.len(),
474    ///         "UTF-8 decoder capacity bound must consume the complete token"
475    ///     );
476    ///
477    ///     // Print the resulting partially decoded token.
478    ///     println!("{token} --> {token_string}");
479    /// }
480    /// ```
481    pub fn token_to_piece_into(
482        &self,
483        token: LlamaToken,
484        buffer: &mut Vec<u8>,
485        special: bool,
486        lstrip: Option<NonZeroU16>,
487    ) {
488        let lstrip = lstrip.map_or(0, |it| i32::from(it.get()));
489        let result = self.token_to_piece_raw(token, buffer.spare_capacity_mut(), special, lstrip);
490
491        let size = match result {
492            result if result.is_negative() => {
493                // Cannot overflow, usize > i32, and `-written` is non-negative.
494                let required_size = (-result) as usize;
495
496                // If we fail the first time we resize the vector to the
497                // correct size and try again.
498                buffer.reserve_exact(required_size);
499
500                let result =
501                    self.token_to_piece_raw(token, buffer.spare_capacity_mut(), special, lstrip);
502
503                // Should succeed, we know the vector was correctly sized now.
504                debug_assert!(result.is_positive());
505
506                result as usize
507            }
508            // Cannot overflow, usize > i32
509            written => written as usize,
510        };
511
512        // SAFETY: llama.cpp has written `size` items into the spare capacity.
513        unsafe { buffer.set_len(buffer.len() + size) };
514    }
515
516    fn token_to_piece_raw(
517        &self,
518        token: LlamaToken,
519        buffer: &mut [MaybeUninit<u8>],
520        special: bool,
521        lstrip: i32,
522    ) -> i32 {
523        unsafe {
524            llama_cpp_sys_2::llama_token_to_piece(
525                self.vocab.as_ptr(),
526                token.0,
527                buffer.as_mut_ptr().cast::<c_char>(),
528                // It's fine to clamp the buffer length here, we'll get an
529                // error from llama.cpp anyhow if there are too many tokens to
530                // detokenize.
531                buffer.len().try_into().unwrap_or(i32::MAX),
532                lstrip,
533                special,
534            )
535        }
536    }
537
538    /// Get all tokens in the vocabulary.
539    pub fn tokens(&self) -> impl Iterator<Item = LlamaToken> {
540        (0..self.n_tokens()).map(LlamaToken::new)
541    }
542}
543
544/// a rusty equivalent of `llama_vocab_type`
545#[repr(u32)]
546#[derive(Debug, Eq, Copy, Clone, PartialEq)]
547pub enum VocabType {
548    /// For models without vocab.
549    NONE = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_NONE as _,
550    /// LLaMA tokenizer based on byte-level BPE with byte fallback.
551    SPM = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_SPM as _,
552    /// GPT-2 tokenizer based on byte-level BPE.
553    BPE = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_BPE as _,
554    /// BERT tokenizer based on WordPiece
555    WPM = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_WPM as _,
556    /// T5 tokenizer based on Unigram
557    UGM = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_UGM as _,
558    /// RWKV tokenizer based on greedy tokenization
559    RWKV = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_RWKV as _,
560    /// PLaMo-2 tokenizer based on Aho-Corasick with dynamic programming
561    PLAMO2 = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_PLAMO2 as _,
562    /// Dummy tokenizer for testing: rolling hash of fixed-size chunks -> tokens, tokens -> hex
563    TEST = llama_cpp_sys_2::LLAMA_VOCAB_TYPE_TEST as _,
564}
565
566/// There was an error converting a `llama_vocab_type` to a `VocabType`.
567#[derive(thiserror::Error, Debug, Eq, PartialEq)]
568pub enum LlamaTokenTypeFromIntError {
569    /// The value is not a valid `llama_token_type`. Contains the int value that was invalid.
570    #[error("Unknown Value {0}")]
571    UnknownValue(llama_cpp_sys_2::llama_vocab_type),
572}
573
574impl TryFrom<llama_cpp_sys_2::llama_vocab_type> for VocabType {
575    type Error = LlamaTokenTypeFromIntError;
576
577    fn try_from(value: llama_cpp_sys_2::llama_vocab_type) -> Result<Self, Self::Error> {
578        match value {
579            llama_cpp_sys_2::LLAMA_VOCAB_TYPE_BPE => Ok(VocabType::BPE),
580            llama_cpp_sys_2::LLAMA_VOCAB_TYPE_SPM => Ok(VocabType::SPM),
581            unknown => Err(LlamaTokenTypeFromIntError::UnknownValue(unknown)),
582        }
583    }
584}