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}