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.
- checksums.yaml +4 -4
- data/Cargo.lock +2 -2
- data/Cargo.toml +1 -1
- data/README.md +54 -11
- data/ext/gigatoken/Cargo.toml +1 -1
- data/ext/gigatoken/src/cache.rs +37 -0
- data/ext/gigatoken/src/lib.rs +25 -0
- data/ext/gigatoken/src/sentencepiece.rs +12 -1
- data/ext/gigatoken/src/tokenizer.rs +33 -4
- data/lib/gigatoken/cli/bench.rb +4 -3
- data/lib/gigatoken/cli/support.rb +7 -4
- data/lib/gigatoken/cli/validate.rb +4 -3
- data/lib/gigatoken/encodings/PROVENANCE.md +110 -0
- data/lib/gigatoken/encodings/cl100k_base.tiktoken +100256 -0
- data/lib/gigatoken/encodings/o200k_base.tiktoken +199998 -0
- data/lib/gigatoken/encodings/r50k_base.tiktoken +50256 -0
- data/lib/gigatoken/encodings.rb +103 -0
- data/lib/gigatoken/tokenizer.rb +48 -11
- data/lib/gigatoken/version.rb +1 -1
- data/lib/gigatoken.rb +15 -0
- data/src/batch.rs +2 -7
- data/src/bindings/cache.rs +38 -0
- data/src/bindings/mod.rs +3 -1
- data/src/bindings/pretokenize.rs +11 -0
- data/src/bpe/pretoken_cache.rs +32 -1
- data/src/bpe/sentencepiece.rs +137 -5
- data/src/bpe/tiktoken.rs +784 -31
- data/src/lib.rs +52 -35
- data/src/load_tokenizer/hf.rs +1 -0
- data/src/load_tokenizer/tiktoken.rs +17 -49
- data/src/main.rs +6 -2
- data/src/pretokenize/options.rs +16 -1
- metadata +9 -1
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::{
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 =
|
|
103
|
-
|
|
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:
|
|
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(
|
|
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) =>
|
|
541
|
-
py,
|
|
542
|
-
|
|
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
|
}
|
data/src/load_tokenizer/hf.rs
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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)
|
data/src/pretokenize/options.rs
CHANGED
|
@@ -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
|
-
///
|
|
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.
|
|
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: []
|