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}