gigatoken 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/src/lib.rs CHANGED
@@ -32,7 +32,9 @@ use crate::bindings::matcher::{SpecialTokenFound, SubstringMatcher};
32
32
  #[cfg(feature = "python")]
33
33
  use crate::bindings::padding;
34
34
  #[cfg(feature = "python")]
35
- use crate::bindings::pretokenize::{PretokenizerIter, pretokenized_counts, pretokenizer};
35
+ use crate::bindings::pretokenize::{
36
+ PretokenizerIter, pretokenized_counts, pretokenizer, pretokenizer_scheme,
37
+ };
36
38
  #[cfg(feature = "python")]
37
39
  use crate::bindings::sources::{
38
40
  BytesSource, FileSource, JsonlFileSource, ParquetFileSource, TextFileSource,
@@ -51,6 +53,8 @@ use pyo3::pybacked::{PyBackedBytes, PyBackedStr};
51
53
  #[cfg(feature = "python")]
52
54
  use pyo3::types::{PyBytes, PyDict, PyList};
53
55
  #[cfg(feature = "python")]
56
+ use std::collections::HashMap;
57
+ #[cfg(feature = "python")]
54
58
  use std::path::PathBuf;
55
59
 
56
60
  #[cfg(feature = "python")]
@@ -83,30 +87,24 @@ impl BPETokenizer {
83
87
  #[cfg(feature = "python")]
84
88
  #[pymethods]
85
89
  impl BPETokenizer {
90
+ /// Load from a .tiktoken rank file with the named pretokenizer scheme
91
+ /// and, optionally, a {content: id} mapping of special tokens. The file
92
+ /// carries neither, so both are the caller's to supply — see
93
+ /// `gigatoken.Tokenizer.from_tiktoken`, which knows them for the
94
+ /// encodings OpenAI publishes.
86
95
  #[staticmethod]
87
- fn from_tiktoken(path: PathBuf) -> PyResult<Self> {
88
- Ok(Self {
89
- tokenizer: load_tokenizer::tiktoken::load_tiktoken(&path)?,
90
- workers: WorkerPool::new(),
91
- })
92
- }
93
- /// Load from a tiktoken rank file plus a tokenizer_config.json carrying
94
- /// the special tokens — the layout of repos that ship no tokenizer.json
95
- /// (e.g. the moonshotai Kimi line) — with the named pretokenizer scheme.
96
- #[staticmethod]
97
- fn from_tiktoken_model(
98
- model_path: PathBuf,
99
- config_path: PathBuf,
96
+ #[pyo3(signature = (path, pretokenizer, special_tokens = None))]
97
+ fn from_tiktoken(
98
+ path: PathBuf,
100
99
  pretokenizer: &str,
100
+ special_tokens: Option<HashMap<String, u32>>,
101
101
  ) -> PyResult<Self> {
102
- let scheme = pretokenize::PretokenizerType::from_name(pretokenizer).ok_or_else(|| {
103
- pyo3::exceptions::PyValueError::new_err(format!(
104
- "unknown pretokenizer scheme {pretokenizer:?}; expected one of \
105
- gpt2, gpt4, qwen2, qwen35, olmo3, deepseek_v3, o200k, nemotron, kimi"
106
- ))
107
- })?;
102
+ let scheme = pretokenizer_scheme(pretokenizer)?;
103
+ let special_tokens = special_tokens.unwrap_or_default().into_iter().collect();
108
104
  Ok(Self {
109
- tokenizer: load_tokenizer::tiktoken::load_tiktoken_model(&model_path, &config_path, scheme)?,
105
+ tokenizer: bindings::cache::apply_max_cache_bytes(
106
+ load_tokenizer::tiktoken::load_tiktoken(&path, scheme, special_tokens)?,
107
+ ),
110
108
  workers: WorkerPool::new(),
111
109
  })
112
110
  }
@@ -114,7 +112,9 @@ impl BPETokenizer {
114
112
  #[staticmethod]
115
113
  fn from_hf(path: PathBuf) -> PyResult<Self> {
116
114
  Ok(Self {
117
- tokenizer: load_tokenizer::hf::load_hf_bpe(&path)?,
115
+ tokenizer: bindings::cache::apply_max_cache_bytes(load_tokenizer::hf::load_hf_bpe(
116
+ &path,
117
+ )?),
118
118
  workers: WorkerPool::new(),
119
119
  })
120
120
  }
@@ -276,6 +276,14 @@ impl BPETokenizer {
276
276
  Ok(self.tokenizer.decode(ids.as_slice()?).collect())
277
277
  }
278
278
 
279
+ /// Cached pretoken entries on the single-document `encode` path's
280
+ /// tokenizer (batch workers keep their own caches): grows as text is
281
+ /// encoded, drops back toward vocab-seed level when a budgeted cache
282
+ /// wipes (see gigatoken.set_max_cache_bytes).
283
+ fn cache_entries(&self) -> usize {
284
+ self.tokenizer.cache_entries()
285
+ }
286
+
279
287
  fn __repr__(&self) -> PyResult<String> {
280
288
  Ok(format!("{:?}", self.tokenizer))
281
289
  }
@@ -344,6 +352,15 @@ impl SentencePieceTokenizer {
344
352
  }
345
353
  })
346
354
  }
355
+
356
+ /// Wrap a loaded model, applying the global cache budget to it and to
357
+ /// the single-document encode state.
358
+ fn with_model(model: bpe::SentencePieceBPE) -> Self {
359
+ let tokenizer = bindings::cache::apply_max_cache_bytes_sp(model);
360
+ let state =
361
+ bpe::sentencepiece::EncodeState::with_budget(tokenizer.max_cache_bytes());
362
+ Self { tokenizer, state }
363
+ }
347
364
  }
348
365
 
349
366
  #[cfg(feature = "python")]
@@ -351,10 +368,7 @@ impl SentencePieceTokenizer {
351
368
  impl SentencePieceTokenizer {
352
369
  #[staticmethod]
353
370
  fn from_hf(path: PathBuf) -> PyResult<Self> {
354
- Ok(Self {
355
- tokenizer: load_tokenizer::hf::load_hf_sentencepiece(&path)?,
356
- state: bpe::sentencepiece::EncodeState::new(),
357
- })
371
+ Ok(Self::with_model(load_tokenizer::hf::load_hf_sentencepiece(&path)?))
358
372
  }
359
373
 
360
374
  /// Encode a batch of documents in parallel, releasing the GIL. Accepts
@@ -502,6 +516,12 @@ impl SentencePieceTokenizer {
502
516
  Ok(self.tokenizer.decode(ids.as_slice()?))
503
517
  }
504
518
 
519
+ /// Cached unit entries on the single-document `encode` path's state
520
+ /// (batch encoders are per-call); see `BPETokenizer.cache_entries`.
521
+ fn cache_entries(&self) -> usize {
522
+ self.state.cache_size()
523
+ }
524
+
505
525
  fn __repr__(&self) -> PyResult<String> {
506
526
  Ok(format!("{:?}", self.tokenizer))
507
527
  }
@@ -532,19 +552,14 @@ fn load_hf_json(py: Python<'_>, data: Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
532
552
  load_tokenizer::hf::HfTokenizer::Bpe(tokenizer) => Ok(Py::new(
533
553
  py,
534
554
  BPETokenizer {
535
- tokenizer,
555
+ tokenizer: bindings::cache::apply_max_cache_bytes(tokenizer),
536
556
  workers: WorkerPool::new(),
537
557
  },
538
558
  )?
539
559
  .into_any()),
540
- load_tokenizer::hf::HfTokenizer::SentencePiece(tokenizer) => Ok(Py::new(
541
- py,
542
- SentencePieceTokenizer {
543
- tokenizer,
544
- state: bpe::sentencepiece::EncodeState::new(),
545
- },
546
- )?
547
- .into_any()),
560
+ load_tokenizer::hf::HfTokenizer::SentencePiece(tokenizer) => {
561
+ Ok(Py::new(py, SentencePieceTokenizer::with_model(tokenizer))?.into_any())
562
+ }
548
563
  }
549
564
  }
550
565
 
@@ -574,5 +589,7 @@ fn gigatoken_rs<'py>(py: Python, m: &Bound<'py, PyModule>) -> PyResult<()> {
574
589
  m.add_function(wrap_pyfunction!(bindings::hub::hub_file, m)?)?;
575
590
  m.add_function(wrap_pyfunction!(bindings::hub::looks_like_repo_id, m)?)?;
576
591
  m.add_function(wrap_pyfunction!(bindings::hub::get_hf_token, m)?)?;
592
+ m.add_function(wrap_pyfunction!(bindings::cache::set_max_cache_bytes, m)?)?;
593
+ m.add_function(wrap_pyfunction!(bindings::cache::get_max_cache_bytes, m)?)?;
577
594
  Ok(())
578
595
  }
@@ -413,6 +413,7 @@ fn build_sentencepiece(tj: &TokenizerJson) -> Result<SentencePieceBPE> {
413
413
  split_safe: Vec::new(),
414
414
  cross_pieces: Vec::new(),
415
415
  cross_prev: [0; 4],
416
+ max_cache_bytes: Some(crate::bpe::Tokenizer::DEFAULT_MAX_CACHE_BYTES),
416
417
  };
417
418
  model.norm_added_tokens = norm_added_tokens
418
419
  .into_iter()
@@ -26,62 +26,30 @@ fn load_tiktoken_ranks(file_path: impl AsRef<Path>) -> Result<Vec<Vec<u8>>> {
26
26
  .collect()
27
27
  }
28
28
 
29
- pub fn load_tiktoken(file_path: impl AsRef<Path>) -> Result<Tokenizer> {
30
- let rank_vocab = load_tiktoken_ranks(file_path)?;
31
- let n_ranks = rank_vocab.len() as u32;
32
- let mut tokenizer = Tokenizer::from_ranks(rank_vocab)?;
33
- // Tiktoken vocab files carry no special tokens; GPT-2-family vocabs
34
- // (gpt2/r50k) place <|endoftext|> at the id right after the mergeable
35
- // ranks. Register it so tiktoken- and tokenizer.json-loaded tokenizers
36
- // encode and decode identically.
37
- tokenizer.add_special_token(b"<|endoftext|>".to_vec(), n_ranks.into());
38
- Ok(tokenizer)
39
- }
40
-
41
- /// The fields of a HuggingFace tokenizer_config.json a tiktoken-rank repo
42
- /// needs: the special tokens (there is no tokenizer.json to carry them).
43
- #[derive(serde::Deserialize)]
44
- struct TokenizerConfigJson {
45
- #[serde(default)]
46
- added_tokens_decoder: std::collections::BTreeMap<String, AddedTokenJson>,
47
- }
48
-
49
- #[derive(serde::Deserialize)]
50
- struct AddedTokenJson {
51
- content: String,
52
- }
53
-
54
- /// Load a tokenizer from a tiktoken rank file plus a HuggingFace
55
- /// `tokenizer_config.json` whose `added_tokens_decoder` carries the special
56
- /// tokens — the layout of repos that ship no tokenizer.json (e.g. the
57
- /// moonshotai Kimi/Moonlight line's `tiktoken.model`). The pretokenizer
58
- /// scheme is passed by the caller: such repos define their split regex only
59
- /// in remote code, so no shipped file can name it.
60
- pub fn load_tiktoken_model(
61
- model_path: impl AsRef<Path>,
62
- config_path: impl AsRef<Path>,
29
+ /// Load a tokenizer from a tiktoken rank file with the pretokenizer scheme
30
+ /// and special tokens (`(content, id)`) supplied by the caller. A .tiktoken
31
+ /// file carries neither — its split regex and specials live in the code
32
+ /// that defines the encoding — and a wrong scheme silently changes every
33
+ /// encode, so neither may be defaulted here.
34
+ pub fn load_tiktoken(
35
+ file_path: impl AsRef<Path>,
63
36
  pretokenizer: PretokenizerType,
37
+ special_tokens: Vec<(String, u32)>,
64
38
  ) -> Result<Tokenizer> {
65
- let rank_vocab = load_tiktoken_ranks(model_path)?;
39
+ let rank_vocab = load_tiktoken_ranks(file_path)?;
66
40
  let n_ranks = rank_vocab.len() as u32;
67
41
  let mut tokenizer = Tokenizer::from_ranks(rank_vocab)?;
68
42
  tokenizer.set_pretokenizer_type(pretokenizer);
69
- let config_path = config_path.as_ref();
70
- let config: TokenizerConfigJson = sonic_rs::from_slice(
71
- &std::fs::read(config_path)
72
- .with_context(|| format!("Failed to read {}", config_path.display()))?,
73
- )
74
- .map_err(|e| eyre::eyre!("Failed to parse {}: {e}", config_path.display()))?;
75
- for (id, token) in &config.added_tokens_decoder {
76
- let id = id.parse::<u32>().with_context(|| {
77
- format!("added_tokens_decoder id {id:?} in {}", config_path.display())
78
- })?;
43
+ for (content, id) in &special_tokens {
79
44
  ensure!(
80
- id >= n_ranks,
81
- "added token {:?} (id {id}) overlaps the {n_ranks} mergeable ranks",
82
- token.content
45
+ *id >= n_ranks,
46
+ "special token {content:?} (id {id}) overlaps the {n_ranks} mergeable ranks"
83
47
  );
84
- tokenizer.add_special_token(token.content.clone().into_bytes(), id.into());
85
48
  }
49
+ tokenizer.add_special_tokens(
50
+ special_tokens
51
+ .into_iter()
52
+ .map(|(content, id)| (content.into_bytes(), id.into())),
53
+ );
86
54
  Ok(tokenizer)
87
55
  }
data/src/main.rs CHANGED
@@ -73,8 +73,12 @@ pub fn main() {
73
73
  .unwrap()
74
74
  .join("data/tokenizers/r50k_base.tiktoken")
75
75
  });
76
- let mut r50k =
77
- load_tokenizer::tiktoken::load_tiktoken(&r50k_path).expect("Failed to load r50k tokenizer");
76
+ let mut r50k = load_tokenizer::tiktoken::load_tiktoken(
77
+ &r50k_path,
78
+ pretokenize::PretokenizerType::GPT2,
79
+ vec![("<|endoftext|>".to_string(), 50256)],
80
+ )
81
+ .expect("Failed to load r50k tokenizer");
78
82
  eprintln!("Loaded r50k: {:?}", r50k);
79
83
 
80
84
  // Encode with r50k (GPT-2 style: pretokenize + memoized BPE)
@@ -66,10 +66,25 @@ impl PretokenizerType {
66
66
  }
67
67
  }
68
68
 
69
+ /// The canonical name of each variant, in variant order — the
70
+ /// identifiers `from_name` accepts (plus the aliases listed there).
71
+ /// Error messages naming the valid schemes derive from this list.
72
+ pub const NAMES: [&'static str; 9] = [
73
+ "gpt2",
74
+ "gpt4",
75
+ "qwen2",
76
+ "qwen35",
77
+ "olmo3",
78
+ "deepseek_v3",
79
+ "o200k",
80
+ "nemotron",
81
+ "kimi",
82
+ ];
83
+
69
84
  /// The scheme named by a lowercase identifier, as used by loaders whose
70
85
  /// source format carries no split regex (e.g. tiktoken-rank repos, whose
71
86
  /// regex lives in remote code) and the Python bindings. Accepts each
72
- /// variant's canonical name plus the common tiktoken aliases.
87
+ /// canonical name from [`Self::NAMES`] plus the common tiktoken aliases.
73
88
  pub fn from_name(name: &str) -> Option<Self> {
74
89
  Some(match name {
75
90
  "gpt2" | "r50k" => PretokenizerType::GPT2,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gigatoken
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs
@@ -68,6 +68,7 @@ files:
68
68
  - exe/gigatoken
69
69
  - ext/gigatoken/Cargo.toml
70
70
  - ext/gigatoken/extconf.rb
71
+ - ext/gigatoken/src/cache.rs
71
72
  - ext/gigatoken/src/error.rs
72
73
  - ext/gigatoken/src/gvl.rs
73
74
  - ext/gigatoken/src/lib.rs
@@ -79,6 +80,11 @@ files:
79
80
  - lib/gigatoken/cli/bench.rb
80
81
  - lib/gigatoken/cli/support.rb
81
82
  - lib/gigatoken/cli/validate.rb
83
+ - lib/gigatoken/encodings.rb
84
+ - lib/gigatoken/encodings/PROVENANCE.md
85
+ - lib/gigatoken/encodings/cl100k_base.tiktoken
86
+ - lib/gigatoken/encodings/o200k_base.tiktoken
87
+ - lib/gigatoken/encodings/r50k_base.tiktoken
82
88
  - lib/gigatoken/hub.rb
83
89
  - lib/gigatoken/packed_result.rb
84
90
  - lib/gigatoken/tokenizer.rb
@@ -86,6 +92,7 @@ files:
86
92
  - rust-toolchain.toml
87
93
  - src/batch.rs
88
94
  - src/bindings/bridge.rs
95
+ - src/bindings/cache.rs
89
96
  - src/bindings/hub.rs
90
97
  - src/bindings/matcher.rs
91
98
  - src/bindings/mod.rs
@@ -139,6 +146,7 @@ licenses:
139
146
  - MIT
140
147
  metadata:
141
148
  bug_tracker_uri: https://github.com/jetpks/gigatoken-rb/issues
149
+ changelog_uri: https://github.com/jetpks/gigatoken-rb/blob/main/CHANGELOG.md
142
150
  homepage_uri: https://github.com/jetpks/gigatoken-rb
143
151
  source_code_uri: https://github.com/jetpks/gigatoken-rb
144
152
  rdoc_options: []