gigatoken 0.1.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 +7 -0
- data/Cargo.lock +3016 -0
- data/Cargo.toml +135 -0
- data/LICENSE +21 -0
- data/README.md +141 -0
- data/exe/gigatoken +9 -0
- data/ext/gigatoken/Cargo.toml +24 -0
- data/ext/gigatoken/extconf.rb +19 -0
- data/ext/gigatoken/src/error.rs +20 -0
- data/ext/gigatoken/src/gvl.rs +122 -0
- data/ext/gigatoken/src/lib.rs +50 -0
- data/ext/gigatoken/src/sentencepiece.rs +205 -0
- data/ext/gigatoken/src/sources.rs +207 -0
- data/ext/gigatoken/src/tokenizer.rs +571 -0
- data/lib/gigatoken/cli/bench.rb +64 -0
- data/lib/gigatoken/cli/support.rb +132 -0
- data/lib/gigatoken/cli/validate.rb +55 -0
- data/lib/gigatoken/cli.rb +18 -0
- data/lib/gigatoken/hub.rb +242 -0
- data/lib/gigatoken/packed_result.rb +48 -0
- data/lib/gigatoken/tokenizer.rb +117 -0
- data/lib/gigatoken/version.rb +5 -0
- data/lib/gigatoken.rb +29 -0
- data/rust-toolchain.toml +8 -0
- data/src/batch.rs +1808 -0
- data/src/bindings/bridge.rs +396 -0
- data/src/bindings/hub.rs +42 -0
- data/src/bindings/matcher.rs +114 -0
- data/src/bindings/mod.rs +14 -0
- data/src/bindings/padding.rs +177 -0
- data/src/bindings/pretokenize.rs +53 -0
- data/src/bindings/sources.rs +273 -0
- data/src/bindings/train.rs +125 -0
- data/src/bpe/mod.rs +1217 -0
- data/src/bpe/pretoken_cache.rs +495 -0
- data/src/bpe/sentencepiece.rs +1485 -0
- data/src/bpe/tiktoken.rs +2555 -0
- data/src/bpe_train.rs +351 -0
- data/src/input/decompress.rs +11 -0
- data/src/input/file_source.rs +514 -0
- data/src/input/jsonl.rs +94 -0
- data/src/input/mod.rs +333 -0
- data/src/input/parquet.rs +303 -0
- data/src/lib.rs +578 -0
- data/src/load_tokenizer/hf.rs +1036 -0
- data/src/load_tokenizer/hub.rs +344 -0
- data/src/load_tokenizer/mod.rs +3 -0
- data/src/load_tokenizer/tiktoken.rs +87 -0
- data/src/main.rs +95 -0
- data/src/pretokenize/fast/cl100k.rs +426 -0
- data/src/pretokenize/fast/cl100k_family.rs +891 -0
- data/src/pretokenize/fast/deepseek_v3.rs +605 -0
- data/src/pretokenize/fast/kimi.rs +281 -0
- data/src/pretokenize/fast/mask.rs +1486 -0
- data/src/pretokenize/fast/mod.rs +446 -0
- data/src/pretokenize/fast/nemotron.rs +138 -0
- data/src/pretokenize/fast/o200k.rs +347 -0
- data/src/pretokenize/fast/o200k_family.rs +1734 -0
- data/src/pretokenize/fast/olmo3.rs +505 -0
- data/src/pretokenize/fast/qwen2.rs +429 -0
- data/src/pretokenize/fast/qwen3_5.rs +541 -0
- data/src/pretokenize/fast/r50k.rs +1250 -0
- data/src/pretokenize/mod.rs +1079 -0
- data/src/pretokenize/options.rs +188 -0
- data/src/pretokenize/pretoken.rs +20 -0
- data/src/pretokenize/pretokenize_traits.rs +49 -0
- data/src/pretokenize/reference/avx512.rs +522 -0
- data/src/pretokenize/reference/combinator.rs +572 -0
- data/src/pretokenize/reference/mod.rs +28 -0
- data/src/pretokenize/reference/simd.rs +852 -0
- data/src/pretokenize/reference/state_machine.rs +365 -0
- data/src/pretokenize/unicode.rs +546 -0
- data/src/test_hub.rs +28 -0
- data/src/token.rs +42 -0
- metadata +161 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//! Padded/truncated batch assembly, serving the drop-in compatibility APIs
|
|
2
|
+
//! (gigatoken.HFCompat's padding/truncation support). Kept out of lib.rs so
|
|
3
|
+
//! the main encode path stays easy to read: the `encode_batch_padded`
|
|
4
|
+
//! bindings there are one-call forwards into this module, and the options
|
|
5
|
+
//! travel as the Rust-defined `PadTruncate` class, so per call they cost a
|
|
6
|
+
//! typed downcast rather than dict-key lookups. The friendly keyword
|
|
7
|
+
//! signature lives on `gigatoken.Tokenizer.encode_batch_padded`.
|
|
8
|
+
|
|
9
|
+
use crate::input::file_source::DocFormat;
|
|
10
|
+
use numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods};
|
|
11
|
+
use pyo3::prelude::*;
|
|
12
|
+
|
|
13
|
+
/// How `encode_batch_padded` assembles rows into a rectangular matrix.
|
|
14
|
+
/// A frozen pyclass: fields are validated once at construction, and bad
|
|
15
|
+
/// names or types fail there with a TypeError instead of at encode time.
|
|
16
|
+
#[pyclass(frozen, get_all, from_py_object)]
|
|
17
|
+
#[derive(Clone)]
|
|
18
|
+
pub struct PadTruncate {
|
|
19
|
+
pub pad_id: u32,
|
|
20
|
+
/// Truncation limit and/or fixed pad width, depending on the two flags.
|
|
21
|
+
pub max_length: Option<usize>,
|
|
22
|
+
/// Pad every row to exactly `max_length` instead of the longest row.
|
|
23
|
+
pub pad_to_max_length: bool,
|
|
24
|
+
/// Keep at most `max_length` ids per row, counting prefix and suffix.
|
|
25
|
+
pub truncate: bool,
|
|
26
|
+
/// Put the padding before the tokens instead of after them.
|
|
27
|
+
pub pad_left: bool,
|
|
28
|
+
/// Drop tokens from the start of a row instead of the end.
|
|
29
|
+
pub truncate_left: bool,
|
|
30
|
+
/// Special-token ids written before / after every row's tokens; they
|
|
31
|
+
/// count toward the truncation budget, like HF post-processors.
|
|
32
|
+
pub prefix: Vec<u32>,
|
|
33
|
+
pub suffix: Vec<u32>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[pymethods]
|
|
37
|
+
impl PadTruncate {
|
|
38
|
+
#[new]
|
|
39
|
+
#[allow(clippy::too_many_arguments)]
|
|
40
|
+
#[pyo3(signature = (pad_id, max_length=None, pad_to_max_length=false, truncate=false, pad_left=false, truncate_left=false, prefix=Vec::new(), suffix=Vec::new()))]
|
|
41
|
+
fn new(
|
|
42
|
+
pad_id: u32,
|
|
43
|
+
max_length: Option<usize>,
|
|
44
|
+
pad_to_max_length: bool,
|
|
45
|
+
truncate: bool,
|
|
46
|
+
pad_left: bool,
|
|
47
|
+
truncate_left: bool,
|
|
48
|
+
prefix: Vec<u32>,
|
|
49
|
+
suffix: Vec<u32>,
|
|
50
|
+
) -> Self {
|
|
51
|
+
Self {
|
|
52
|
+
pad_id,
|
|
53
|
+
max_length,
|
|
54
|
+
pad_to_max_length,
|
|
55
|
+
truncate,
|
|
56
|
+
pad_left,
|
|
57
|
+
truncate_left,
|
|
58
|
+
prefix,
|
|
59
|
+
suffix,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn __repr__(&self) -> String {
|
|
64
|
+
let py_bool = |b: bool| if b { "True" } else { "False" };
|
|
65
|
+
let max_length = self
|
|
66
|
+
.max_length
|
|
67
|
+
.map_or("None".to_string(), |m| m.to_string());
|
|
68
|
+
format!(
|
|
69
|
+
"PadTruncate(pad_id={}, max_length={max_length}, pad_to_max_length={}, truncate={}, \
|
|
70
|
+
pad_left={}, truncate_left={}, prefix={:?}, suffix={:?})",
|
|
71
|
+
self.pad_id,
|
|
72
|
+
py_bool(self.pad_to_max_length),
|
|
73
|
+
py_bool(self.truncate),
|
|
74
|
+
py_bool(self.pad_left),
|
|
75
|
+
py_bool(self.truncate_left),
|
|
76
|
+
self.prefix,
|
|
77
|
+
self.suffix,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The (rows x width) id matrix plus each row's real (unpadded) length —
|
|
83
|
+
/// everything an attention mask needs.
|
|
84
|
+
pub type PaddedMatrix<'py> = (Bound<'py, PyArray2<u32>>, Bound<'py, PyArray1<i64>>);
|
|
85
|
+
|
|
86
|
+
/// Shared back-end of the `encode_batch_padded` bindings: encode like
|
|
87
|
+
/// encode_batch, then pad/truncate into one (rows x width) uint32 matrix.
|
|
88
|
+
pub fn encode_batch_matrix<'py>(
|
|
89
|
+
py: Python<'py>,
|
|
90
|
+
inputs: &Bound<'py, PyAny>,
|
|
91
|
+
opts: PadTruncate,
|
|
92
|
+
parallel: bool,
|
|
93
|
+
encode: impl Fn(&[&[u8]], &DocFormat) -> PyResult<(Vec<u32>, Vec<i64>)> + Send + Sync,
|
|
94
|
+
) -> PyResult<PaddedMatrix<'py>> {
|
|
95
|
+
let (flat, counts) = super::bridge::encode_batch_flat(py, inputs, encode)?;
|
|
96
|
+
let (data, lengths, width) = py.detach(|| pad_truncate_matrix(&flat, &counts, &opts, parallel))?;
|
|
97
|
+
let rows = lengths.len();
|
|
98
|
+
let matrix = data.into_pyarray(py).reshape([rows, width])?;
|
|
99
|
+
Ok((matrix, lengths.into_pyarray(py)))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Assemble a ragged token batch into one row-major (rows x width) matrix in
|
|
103
|
+
/// a single parallel copy pass. Each row is `prefix` + the (optionally
|
|
104
|
+
/// truncated) tokens + `suffix`, padded with `pad_id` to the longest row (or
|
|
105
|
+
/// to exactly `max_length` when `pad_to_max_length`). Returns the matrix
|
|
106
|
+
/// data, each row's real (unpadded) length, and the width.
|
|
107
|
+
fn pad_truncate_matrix(
|
|
108
|
+
flat: &[u32],
|
|
109
|
+
counts: &[i64],
|
|
110
|
+
opts: &PadTruncate,
|
|
111
|
+
parallel: bool,
|
|
112
|
+
) -> PyResult<(Vec<u32>, Vec<i64>, usize)> {
|
|
113
|
+
use rayon::prelude::*;
|
|
114
|
+
let value_err = |msg: String| PyErr::new::<pyo3::exceptions::PyValueError, _>(msg);
|
|
115
|
+
let extra = opts.prefix.len() + opts.suffix.len();
|
|
116
|
+
let cap = if opts.truncate {
|
|
117
|
+
let max = opts
|
|
118
|
+
.max_length
|
|
119
|
+
.ok_or_else(|| value_err("truncate requires max_length".to_string()))?;
|
|
120
|
+
max.checked_sub(extra).ok_or_else(|| {
|
|
121
|
+
value_err(format!(
|
|
122
|
+
"max_length={max} leaves no room for the {extra} special tokens added per sequence"
|
|
123
|
+
))
|
|
124
|
+
})?
|
|
125
|
+
} else {
|
|
126
|
+
usize::MAX
|
|
127
|
+
};
|
|
128
|
+
let mut offsets = Vec::with_capacity(counts.len());
|
|
129
|
+
let mut lengths = Vec::with_capacity(counts.len());
|
|
130
|
+
let mut longest = 0usize;
|
|
131
|
+
let mut pos = 0usize;
|
|
132
|
+
for &count in counts {
|
|
133
|
+
let kept = (count as usize).min(cap);
|
|
134
|
+
offsets.push(pos);
|
|
135
|
+
pos += count as usize;
|
|
136
|
+
lengths.push((kept + extra) as i64);
|
|
137
|
+
longest = longest.max(kept + extra);
|
|
138
|
+
}
|
|
139
|
+
let width = if opts.pad_to_max_length {
|
|
140
|
+
let max = opts
|
|
141
|
+
.max_length
|
|
142
|
+
.ok_or_else(|| value_err("pad_to_max_length requires max_length".to_string()))?;
|
|
143
|
+
if longest > max {
|
|
144
|
+
return Err(value_err(format!(
|
|
145
|
+
"a sequence is {longest} ids long but padding to max_length={max} was requested \
|
|
146
|
+
without truncation; enable truncation or raise max_length"
|
|
147
|
+
)));
|
|
148
|
+
}
|
|
149
|
+
max
|
|
150
|
+
} else {
|
|
151
|
+
longest
|
|
152
|
+
};
|
|
153
|
+
let mut out = vec![opts.pad_id; counts.len() * width];
|
|
154
|
+
if width > 0 {
|
|
155
|
+
let fill = |(i, row): (usize, &mut [u32])| {
|
|
156
|
+
let count = counts[i] as usize;
|
|
157
|
+
let kept = count.min(cap);
|
|
158
|
+
let tokens = &flat[offsets[i]..offsets[i] + count];
|
|
159
|
+
let tokens = if opts.truncate_left {
|
|
160
|
+
&tokens[count - kept..]
|
|
161
|
+
} else {
|
|
162
|
+
&tokens[..kept]
|
|
163
|
+
};
|
|
164
|
+
let len = kept + extra;
|
|
165
|
+
let start = if opts.pad_left { width - len } else { 0 };
|
|
166
|
+
row[start..start + opts.prefix.len()].copy_from_slice(&opts.prefix);
|
|
167
|
+
row[start + opts.prefix.len()..start + opts.prefix.len() + kept].copy_from_slice(tokens);
|
|
168
|
+
row[start + len - opts.suffix.len()..start + len].copy_from_slice(&opts.suffix);
|
|
169
|
+
};
|
|
170
|
+
if parallel {
|
|
171
|
+
out.par_chunks_mut(width).enumerate().for_each(fill);
|
|
172
|
+
} else {
|
|
173
|
+
out.chunks_mut(width).enumerate().for_each(fill);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
Ok((out, lengths, width))
|
|
177
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//! Python bindings exposing the pretokenizer directly (an iterator over one
|
|
2
|
+
//! document's pretokens and a parallel pretoken-count function) — used for
|
|
3
|
+
//! inspection and tests, not by the encode path.
|
|
4
|
+
|
|
5
|
+
use crate::pretokenize;
|
|
6
|
+
use pyo3::prelude::*;
|
|
7
|
+
use pyo3::types::PyBytes;
|
|
8
|
+
|
|
9
|
+
#[pyclass]
|
|
10
|
+
pub(crate) struct PretokenizerIter {
|
|
11
|
+
/// Byte offset into `bytes`; the pretokenizer is stateless beyond this, so
|
|
12
|
+
/// each `__next__` resumes a fresh `FastR50kPretokenizer` at this position.
|
|
13
|
+
pos: usize,
|
|
14
|
+
bytes: Py<PyBytes>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[pymethods]
|
|
18
|
+
impl PretokenizerIter {
|
|
19
|
+
fn __iter__<'py>(slf: PyRef<'py, Self>) -> PyRef<'py, PretokenizerIter> {
|
|
20
|
+
slf
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn __next__<'py>(&'py mut self, py: Python<'py>) -> Option<&'py [u8]> {
|
|
24
|
+
let bytes: &'py [u8] = self.bytes.as_bytes(py);
|
|
25
|
+
let mut iter = pretokenize::FastR50kPretokenizer::with_pos(bytes, self.pos);
|
|
26
|
+
let result = iter.next();
|
|
27
|
+
self.pos = iter.pos();
|
|
28
|
+
Some(result?.0)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[pyfunction]
|
|
33
|
+
pub(crate) fn pretokenizer<'py>(text: Bound<'py, PyBytes>) -> PyResult<PretokenizerIter> {
|
|
34
|
+
Ok(PretokenizerIter {
|
|
35
|
+
pos: 0,
|
|
36
|
+
bytes: text.into(),
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[pyfunction]
|
|
41
|
+
#[pyo3(signature = (text, separator = None))]
|
|
42
|
+
pub(crate) fn pretokenized_counts<'py>(
|
|
43
|
+
text: Bound<'py, PyBytes>,
|
|
44
|
+
separator: Option<&[u8]>,
|
|
45
|
+
) -> PyResult<Vec<(Bound<'py, PyBytes>, usize)>> {
|
|
46
|
+
let separator = separator.unwrap_or(pretokenize::DEFAULT_SEPARATOR);
|
|
47
|
+
let tokens_counts = pretokenize::pretokenize_par_bytes(text.as_bytes(), separator);
|
|
48
|
+
let tokens_counts = tokens_counts
|
|
49
|
+
.into_iter()
|
|
50
|
+
.map(|(k, v)| (PyBytes::new(text.py(), k.as_ref()), v))
|
|
51
|
+
.collect::<Vec<_>>();
|
|
52
|
+
Ok(tokens_counts)
|
|
53
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
//! The FileSource/BytesSource Python classes and the helpers that turn an
|
|
2
|
+
//! encode_files or train_bpe argument into loaded, format-tagged file
|
|
3
|
+
//! contents.
|
|
4
|
+
|
|
5
|
+
use crate::input::Resource;
|
|
6
|
+
use crate::input::file_source::{DocFormat, LoadedFile, detect_default_format, load_file};
|
|
7
|
+
use pyo3::prelude::*;
|
|
8
|
+
use pyo3::pybacked::PyBackedBytes;
|
|
9
|
+
use std::path::PathBuf;
|
|
10
|
+
|
|
11
|
+
/// Base class for file sources. Not directly constructible from Python —
|
|
12
|
+
/// use `TextFileSource` or `JsonlFileSource`, which pin down the document
|
|
13
|
+
/// format and its parameters. Compression (.gz/.zst) is always detected
|
|
14
|
+
/// from the file extension, independent of the source type.
|
|
15
|
+
#[pyclass(subclass, from_py_object)]
|
|
16
|
+
#[derive(Clone)]
|
|
17
|
+
pub(crate) struct FileSource {
|
|
18
|
+
pub(crate) paths: Vec<PathBuf>,
|
|
19
|
+
pub(crate) format: DocFormat,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[pymethods]
|
|
23
|
+
impl FileSource {
|
|
24
|
+
fn __repr__(&self) -> String {
|
|
25
|
+
let n = self.paths.len();
|
|
26
|
+
match &self.format {
|
|
27
|
+
DocFormat::Jsonl { field } => {
|
|
28
|
+
format!("JsonlFileSource(paths=[{n} files], field={field:?})")
|
|
29
|
+
}
|
|
30
|
+
DocFormat::Text {
|
|
31
|
+
separator: Some(sep),
|
|
32
|
+
} => format!(
|
|
33
|
+
"TextFileSource(paths=[{n} files], separator={:?})",
|
|
34
|
+
String::from_utf8_lossy(sep)
|
|
35
|
+
),
|
|
36
|
+
DocFormat::Text { separator: None } => {
|
|
37
|
+
format!("TextFileSource(paths=[{n} files])")
|
|
38
|
+
}
|
|
39
|
+
DocFormat::Parquet { column } => {
|
|
40
|
+
format!("ParquetFileSource(paths=[{n} files], column={column:?})")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// A `separator` argument: bytes used as-is, or str encoded to its UTF-8
|
|
47
|
+
/// bytes.
|
|
48
|
+
#[derive(FromPyObject)]
|
|
49
|
+
pub(crate) enum Separator {
|
|
50
|
+
Bytes(Vec<u8>),
|
|
51
|
+
Str(String),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl Separator {
|
|
55
|
+
fn into_bytes(self) -> Vec<u8> {
|
|
56
|
+
match self {
|
|
57
|
+
Separator::Bytes(b) => b,
|
|
58
|
+
Separator::Str(s) => s.into_bytes(),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Plain-text files. With `separator` (str or bytes), documents are the
|
|
64
|
+
/// pieces between separator occurrences (the separator itself belongs to no
|
|
65
|
+
/// document); without one, each file is a single document.
|
|
66
|
+
#[pyclass(extends = FileSource)]
|
|
67
|
+
pub(crate) struct TextFileSource;
|
|
68
|
+
|
|
69
|
+
#[pymethods]
|
|
70
|
+
impl TextFileSource {
|
|
71
|
+
#[new]
|
|
72
|
+
#[pyo3(signature = (paths, separator = None))]
|
|
73
|
+
fn new(paths: Vec<PathBuf>, separator: Option<Separator>) -> PyClassInitializer<Self> {
|
|
74
|
+
PyClassInitializer::from(FileSource {
|
|
75
|
+
paths,
|
|
76
|
+
format: DocFormat::Text {
|
|
77
|
+
separator: separator.map(Separator::into_bytes),
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
.add_subclass(Self)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// JSON Lines files: one document per line, text taken from `field`.
|
|
85
|
+
#[pyclass(extends = FileSource)]
|
|
86
|
+
pub(crate) struct JsonlFileSource;
|
|
87
|
+
|
|
88
|
+
#[pymethods]
|
|
89
|
+
impl JsonlFileSource {
|
|
90
|
+
#[new]
|
|
91
|
+
#[pyo3(signature = (paths, field = "text"))]
|
|
92
|
+
fn new(paths: Vec<PathBuf>, field: &str) -> PyClassInitializer<Self> {
|
|
93
|
+
PyClassInitializer::from(FileSource {
|
|
94
|
+
paths,
|
|
95
|
+
format: DocFormat::Jsonl {
|
|
96
|
+
field: field.to_string(),
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
.add_subclass(Self)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Parquet files: one document per row, text taken from `column` (a string
|
|
104
|
+
/// or binary column; null rows become empty documents, so results stay
|
|
105
|
+
/// row-aligned with the table). Parquet handles compression internally, so
|
|
106
|
+
/// no .gz/.zst detection applies.
|
|
107
|
+
#[pyclass(extends = FileSource)]
|
|
108
|
+
pub(crate) struct ParquetFileSource;
|
|
109
|
+
|
|
110
|
+
#[pymethods]
|
|
111
|
+
impl ParquetFileSource {
|
|
112
|
+
#[new]
|
|
113
|
+
#[pyo3(signature = (paths, column = "text"))]
|
|
114
|
+
fn new(paths: Vec<PathBuf>, column: &str) -> PyClassInitializer<Self> {
|
|
115
|
+
PyClassInitializer::from(FileSource {
|
|
116
|
+
paths,
|
|
117
|
+
format: DocFormat::Parquet {
|
|
118
|
+
column: column.to_string(),
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
.add_subclass(Self)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// In-memory bytes for encode_batch, the buffer analog of TextFileSource:
|
|
126
|
+
/// with `separator` (str or bytes), each buffer's documents are the pieces
|
|
127
|
+
/// between separator occurrences (the separator itself belongs to no
|
|
128
|
+
/// document, and empty pieces are skipped); without one, each buffer is a
|
|
129
|
+
/// single document. The buffers are borrowed, not copied, and split during
|
|
130
|
+
/// the (parallel) encode itself — handing a whole corpus over as a few
|
|
131
|
+
/// buffers plus a separator is much faster than pre-splitting it into
|
|
132
|
+
/// per-document Python objects.
|
|
133
|
+
#[pyclass(frozen)]
|
|
134
|
+
pub(crate) struct BytesSource {
|
|
135
|
+
pub(crate) buffers: Vec<PyBackedBytes>,
|
|
136
|
+
pub(crate) format: DocFormat,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[pymethods]
|
|
140
|
+
impl BytesSource {
|
|
141
|
+
#[new]
|
|
142
|
+
#[pyo3(signature = (data, separator = None))]
|
|
143
|
+
fn new(data: &Bound<'_, PyAny>, separator: Option<Separator>) -> PyResult<Self> {
|
|
144
|
+
let buffers = if let Ok(buffer) = data.extract::<PyBackedBytes>() {
|
|
145
|
+
vec![buffer]
|
|
146
|
+
} else {
|
|
147
|
+
data.extract::<Vec<PyBackedBytes>>().map_err(|_| {
|
|
148
|
+
PyErr::new::<pyo3::exceptions::PyTypeError, _>(format!(
|
|
149
|
+
"expected bytes or a list of bytes, got {}",
|
|
150
|
+
data.get_type()
|
|
151
|
+
))
|
|
152
|
+
})?
|
|
153
|
+
};
|
|
154
|
+
Ok(Self {
|
|
155
|
+
buffers,
|
|
156
|
+
format: DocFormat::Text {
|
|
157
|
+
separator: separator.map(Separator::into_bytes),
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn __repr__(&self) -> String {
|
|
163
|
+
let n = self.buffers.len();
|
|
164
|
+
let total: usize = self.buffers.iter().map(|b| b.len()).sum();
|
|
165
|
+
match &self.format {
|
|
166
|
+
DocFormat::Text {
|
|
167
|
+
separator: Some(sep),
|
|
168
|
+
} => format!(
|
|
169
|
+
"BytesSource(data=[{n} buffers, {total} bytes], separator={:?})",
|
|
170
|
+
String::from_utf8_lossy(sep)
|
|
171
|
+
),
|
|
172
|
+
_ => format!("BytesSource(data=[{n} buffers, {total} bytes])"),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Resolve an encode_files argument: a FileSource (TextFileSource /
|
|
178
|
+
/// JsonlFileSource), a single path, or a list of paths. Bare paths get a
|
|
179
|
+
/// default format from the first path's extension — all inputs in a batch
|
|
180
|
+
/// are assumed to be of the same type.
|
|
181
|
+
pub(crate) fn resolve_files_source(obj: &Bound<'_, PyAny>) -> PyResult<(Vec<PathBuf>, DocFormat)> {
|
|
182
|
+
if let Ok(fs) = obj.extract::<FileSource>() {
|
|
183
|
+
return Ok((fs.paths, fs.format));
|
|
184
|
+
}
|
|
185
|
+
if let Ok(path) = obj.extract::<PathBuf>() {
|
|
186
|
+
let format = detect_default_format(&path);
|
|
187
|
+
return Ok((vec![path], format));
|
|
188
|
+
}
|
|
189
|
+
if let Ok(paths) = obj.extract::<Vec<PathBuf>>() {
|
|
190
|
+
let format = paths
|
|
191
|
+
.first()
|
|
192
|
+
.map(|p| detect_default_format(p))
|
|
193
|
+
.unwrap_or(DocFormat::Text { separator: None });
|
|
194
|
+
return Ok((paths, format));
|
|
195
|
+
}
|
|
196
|
+
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(format!(
|
|
197
|
+
"expected a TextFileSource/JsonlFileSource/ParquetFileSource, a path, or a list of paths, got {}",
|
|
198
|
+
obj.get_type()
|
|
199
|
+
)))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Shared scaffold of the encode_files pymethods: resolve the source
|
|
203
|
+
/// argument, load the files with the GIL released, hand their contents and
|
|
204
|
+
/// document format to `encode` (still detached), and return the ragged
|
|
205
|
+
/// result as an awkward Array. The per-backend encoding lives in
|
|
206
|
+
/// `batch::encode_files_docs` / `batch::sp_encode_files_docs`.
|
|
207
|
+
pub(crate) fn encode_files_ragged<'py>(
|
|
208
|
+
py: Python<'py>,
|
|
209
|
+
source: &Bound<'py, PyAny>,
|
|
210
|
+
parallel: bool,
|
|
211
|
+
encode: impl FnOnce(&[&[u8]], &DocFormat) -> (Vec<u32>, Vec<i64>) + Send,
|
|
212
|
+
) -> PyResult<Bound<'py, PyAny>> {
|
|
213
|
+
let (paths, format) = resolve_files_source(source)?;
|
|
214
|
+
let (flat, counts) = py.detach(|| -> PyResult<_> {
|
|
215
|
+
// Parquet rows can't be split out of the raw file bytes, so they are
|
|
216
|
+
// materialized as owned documents here and encoded through the
|
|
217
|
+
// whole-document path (each buffer one document).
|
|
218
|
+
if let DocFormat::Parquet { column } = &format {
|
|
219
|
+
let docs = load_parquet_docs(&paths, column, parallel)?;
|
|
220
|
+
let bytes: Vec<&[u8]> = docs.iter().map(|d| d.as_slice()).collect();
|
|
221
|
+
return Ok(encode(&bytes, &DocFormat::Text { separator: None }));
|
|
222
|
+
}
|
|
223
|
+
let files = load_files(&paths, parallel)?;
|
|
224
|
+
let bytes: Vec<&[u8]> = files.iter().map(|f| f.as_bytes()).collect();
|
|
225
|
+
Ok(encode(&bytes, &format))
|
|
226
|
+
})?;
|
|
227
|
+
super::bridge::ragged_to_python(py, flat, counts)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Load `column` of every parquet file as one owned document per row, files
|
|
231
|
+
/// in argument order, rows in row order. Parallel across files and row
|
|
232
|
+
/// groups with rayon, or fully on the calling thread when `parallel` is
|
|
233
|
+
/// false (the sequential encode paths must never touch the rayon pool).
|
|
234
|
+
fn load_parquet_docs(
|
|
235
|
+
paths: &[PathBuf],
|
|
236
|
+
column: &str,
|
|
237
|
+
parallel: bool,
|
|
238
|
+
) -> PyResult<Vec<Vec<u8>>> {
|
|
239
|
+
use crate::input::parquet::read_docs;
|
|
240
|
+
use rayon::prelude::*;
|
|
241
|
+
// input::parquet errors already carry the offending path.
|
|
242
|
+
let to_pyerr = |e: std::io::Error| PyErr::new::<pyo3::exceptions::PyIOError, _>(e.to_string());
|
|
243
|
+
let per_file: Vec<Vec<Vec<u8>>> = if parallel {
|
|
244
|
+
paths
|
|
245
|
+
.par_iter()
|
|
246
|
+
.map(|p| read_docs(p, column, true).map_err(to_pyerr))
|
|
247
|
+
.collect::<PyResult<_>>()?
|
|
248
|
+
} else {
|
|
249
|
+
paths
|
|
250
|
+
.iter()
|
|
251
|
+
.map(|p| read_docs(p, column, false).map_err(to_pyerr))
|
|
252
|
+
.collect::<PyResult<_>>()?
|
|
253
|
+
};
|
|
254
|
+
Ok(per_file.into_iter().flatten().collect())
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Load all files: mmap when stored uncompressed, decompress .gz/.zst into
|
|
258
|
+
/// memory otherwise (parallel chunking needs random access). In parallel
|
|
259
|
+
/// with rayon, or serially on the calling thread when `parallel` is false
|
|
260
|
+
/// (the sequential encode paths must never touch the rayon pool).
|
|
261
|
+
pub(crate) fn load_files(paths: &[PathBuf], parallel: bool) -> PyResult<Vec<LoadedFile>> {
|
|
262
|
+
use rayon::prelude::*;
|
|
263
|
+
let load = |p: &PathBuf| {
|
|
264
|
+
load_file(p).map_err(|e| {
|
|
265
|
+
PyErr::new::<pyo3::exceptions::PyIOError, _>(format!("{}: {e}", p.display()))
|
|
266
|
+
})
|
|
267
|
+
};
|
|
268
|
+
if parallel {
|
|
269
|
+
paths.par_iter().map(load).collect()
|
|
270
|
+
} else {
|
|
271
|
+
paths.iter().map(load).collect()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//! Python binding for BPE training: the train_bpe function and the
|
|
2
|
+
//! conversion of its result into Python vocab/merges objects.
|
|
3
|
+
|
|
4
|
+
use super::sources::FileSource;
|
|
5
|
+
use crate::bpe_train;
|
|
6
|
+
use crate::input::file_source::FileSourceSpec;
|
|
7
|
+
use crate::input::{MmappedFile, Resource};
|
|
8
|
+
use crate::pretokenize;
|
|
9
|
+
use itertools::Itertools;
|
|
10
|
+
use pyo3::prelude::*;
|
|
11
|
+
use pyo3::types::{IntoPyDict, PyBytes, PyDict};
|
|
12
|
+
use std::path::PathBuf;
|
|
13
|
+
|
|
14
|
+
/// Vocab dict plus ordered merge pairs, as Python objects.
|
|
15
|
+
type PyVocabAndMerges<'py> = (
|
|
16
|
+
Bound<'py, PyDict>,
|
|
17
|
+
Vec<(Bound<'py, PyBytes>, Bound<'py, PyBytes>)>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
fn bpe_result_to_python<'py>(
|
|
21
|
+
py: Python<'py>,
|
|
22
|
+
result: bpe_train::BPEResult,
|
|
23
|
+
) -> PyResult<PyVocabAndMerges<'py>> {
|
|
24
|
+
let vocab_py = result
|
|
25
|
+
.vocab
|
|
26
|
+
.into_iter()
|
|
27
|
+
.map(|(k, v)| (k, PyBytes::new(py, &v)))
|
|
28
|
+
.sorted_by(|e1, e2| Ord::cmp(&e1.0, &e2.0))
|
|
29
|
+
.into_py_dict(py);
|
|
30
|
+
let merges_py: Vec<_> = result
|
|
31
|
+
.merges
|
|
32
|
+
.into_iter()
|
|
33
|
+
.map(|(k, v)| (PyBytes::new(py, &k), PyBytes::new(py, &v)))
|
|
34
|
+
.collect();
|
|
35
|
+
Ok((vocab_py?, merges_py))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn parse_tie_breaking(s: &str) -> PyResult<bpe_train::TieBreaking> {
|
|
39
|
+
match s {
|
|
40
|
+
"huggingface" => Ok(bpe_train::TieBreaking::HuggingFace),
|
|
41
|
+
"raw_token_ids" => Ok(bpe_train::TieBreaking::RawTokenIds),
|
|
42
|
+
"assembled_bytes" => Ok(bpe_train::TieBreaking::AssembledBytes),
|
|
43
|
+
other => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
|
|
44
|
+
"tie_breaking must be 'huggingface', 'raw_token_ids', or 'assembled_bytes', got {other:?}"
|
|
45
|
+
))),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[pyfunction]
|
|
50
|
+
#[allow(clippy::type_complexity)]
|
|
51
|
+
#[pyo3(signature = (in_data, vocab_size, special_tokens, tie_breaking = "huggingface", separator = None))]
|
|
52
|
+
pub(crate) fn train_bpe<'py>(
|
|
53
|
+
py: Python<'py>,
|
|
54
|
+
in_data: Bound<'py, PyAny>,
|
|
55
|
+
vocab_size: usize,
|
|
56
|
+
special_tokens: Vec<String>,
|
|
57
|
+
tie_breaking: &str,
|
|
58
|
+
separator: Option<&[u8]>,
|
|
59
|
+
) -> PyResult<(
|
|
60
|
+
Bound<'py, PyDict>,
|
|
61
|
+
Vec<(Bound<'py, PyBytes>, Bound<'py, PyBytes>)>,
|
|
62
|
+
)> {
|
|
63
|
+
assert!(
|
|
64
|
+
vocab_size <= 2_usize.pow(32),
|
|
65
|
+
"vocab_size must be less than 2^32"
|
|
66
|
+
);
|
|
67
|
+
let tie_breaking = parse_tie_breaking(tie_breaking)?;
|
|
68
|
+
let separator = separator.unwrap_or(pretokenize::DEFAULT_SEPARATOR);
|
|
69
|
+
|
|
70
|
+
// --- FileSource: multi-file parallel processing ---
|
|
71
|
+
if let Ok(file_source) = in_data.extract::<FileSource>() {
|
|
72
|
+
let spec = FileSourceSpec {
|
|
73
|
+
paths: file_source.paths,
|
|
74
|
+
format: file_source.format,
|
|
75
|
+
};
|
|
76
|
+
let counts = spec.pretokenize().map_err(|e| {
|
|
77
|
+
PyErr::new::<pyo3::exceptions::PyIOError, _>(format!(
|
|
78
|
+
"FileSource processing failed: {}",
|
|
79
|
+
e
|
|
80
|
+
))
|
|
81
|
+
})?;
|
|
82
|
+
let result = bpe_train::train_bpe(counts, vocab_size, special_tokens, tie_breaking);
|
|
83
|
+
return bpe_result_to_python(py, result);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Single bytes or file path ---
|
|
87
|
+
let mmap_resource;
|
|
88
|
+
let bytes: &[u8] = if in_data.is_instance_of::<PyBytes>() {
|
|
89
|
+
in_data.extract::<&[u8]>()?
|
|
90
|
+
} else if let Ok(path) = in_data.extract::<PathBuf>() {
|
|
91
|
+
if let Some(ext) = path.extension()
|
|
92
|
+
&& ext == "parquet"
|
|
93
|
+
{
|
|
94
|
+
// A bare path takes the default column "text", matching
|
|
95
|
+
// detect_default_format; use ParquetFileSource to choose another
|
|
96
|
+
// column.
|
|
97
|
+
let spec = FileSourceSpec {
|
|
98
|
+
paths: vec![path],
|
|
99
|
+
format: crate::input::file_source::DocFormat::Parquet {
|
|
100
|
+
column: "text".to_string(),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
let counts = spec
|
|
104
|
+
.pretokenize()
|
|
105
|
+
.map_err(|e| PyErr::new::<pyo3::exceptions::PyIOError, _>(e.to_string()))?;
|
|
106
|
+
let result = bpe_train::train_bpe(counts, vocab_size, special_tokens, tie_breaking);
|
|
107
|
+
return bpe_result_to_python(py, result);
|
|
108
|
+
}
|
|
109
|
+
mmap_resource = MmappedFile::open(&path).map_err(|e| {
|
|
110
|
+
PyErr::new::<pyo3::exceptions::PyIOError, _>(format!(
|
|
111
|
+
"Failed to open file {:?}: {}",
|
|
112
|
+
path, e
|
|
113
|
+
))
|
|
114
|
+
})?;
|
|
115
|
+
mmap_resource.as_bytes()
|
|
116
|
+
} else {
|
|
117
|
+
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
|
|
118
|
+
"in_data must be bytes, a path, or a FileSource",
|
|
119
|
+
));
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let counts = pretokenize::pretokenize_par_bytes(bytes, separator);
|
|
123
|
+
let result = bpe_train::train_bpe(counts, vocab_size, special_tokens, tie_breaking);
|
|
124
|
+
bpe_result_to_python(py, result)
|
|
125
|
+
}
|