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