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,205 @@
|
|
|
1
|
+
//! `Gigatoken::Native::SentencePieceTokenizer`: a `gigatoken::SentencePieceBPE`
|
|
2
|
+
//! plus its persistent pretoken-cache `EncodeState`, and no `WorkerPool` —
|
|
3
|
+
//! the `sp_*` cores parallelize internally instead of through a pool.
|
|
4
|
+
//! Mirrors the pyo3 `SentencePieceTokenizer` in the core crate's `src/lib.rs`
|
|
5
|
+
//! (the `python` feature), minus the numpy/awkward-array machinery that has
|
|
6
|
+
//! no Ruby analog; marshaling helpers are shared with
|
|
7
|
+
//! `Gigatoken::Native::BPETokenizer` (see `crate::tokenizer`).
|
|
8
|
+
//!
|
|
9
|
+
//! Unlike the pyo3 binding, which trusts Python's `str` guarantee, every
|
|
10
|
+
//! document/file byte region handed in from Ruby is validated as UTF-8
|
|
11
|
+
//! before it reaches the `&str`-typed SentencePiece cores — a Ruby String
|
|
12
|
+
//! carries no such guarantee even when tagged UTF-8 — raising
|
|
13
|
+
//! `Gigatoken::Error` instead of ever calling `str::from_utf8_unchecked` on
|
|
14
|
+
//! Ruby-supplied bytes.
|
|
15
|
+
|
|
16
|
+
use std::cell::RefCell;
|
|
17
|
+
|
|
18
|
+
use gigatoken_rs::input::file_source::DocFormat;
|
|
19
|
+
use gigatoken_rs::{EncodeState, SentencePieceBPE, sp_encode_docs_ragged, sp_encode_files_docs, sp_encode_files_docs_serial};
|
|
20
|
+
use magnus::{
|
|
21
|
+
Error, RArray, RClass, RHash, RModule, RString, Ruby, Value, method, prelude::*,
|
|
22
|
+
scan_args::{get_kwargs, scan_args},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
use crate::error::raise;
|
|
26
|
+
use crate::gvl::without_gvl;
|
|
27
|
+
use crate::sources;
|
|
28
|
+
use crate::tokenizer::{binary_string, packed_result, ragged_result};
|
|
29
|
+
|
|
30
|
+
/// Validate that `bytes` is UTF-8, raising `Gigatoken::Error` otherwise.
|
|
31
|
+
fn require_utf8<'a>(ruby: &Ruby, bytes: &'a [u8]) -> Result<&'a str, Error> {
|
|
32
|
+
std::str::from_utf8(bytes).map_err(|e| raise(ruby, format!("invalid UTF-8: {e}")))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[magnus::wrap(class = "Gigatoken::Native::SentencePieceTokenizer", free_immediately, size)]
|
|
36
|
+
pub struct SentencePieceTokenizer {
|
|
37
|
+
// `SentencePieceBPE`'s encode methods take `&self` (only `EncodeState`
|
|
38
|
+
// is mutated), so this `RefCell` is never `borrow_mut`'d — see the
|
|
39
|
+
// builder report's DISAGREEMENTS for why it's here anyway.
|
|
40
|
+
tokenizer: RefCell<SentencePieceBPE>,
|
|
41
|
+
state: RefCell<EncodeState>,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl SentencePieceTokenizer {
|
|
45
|
+
pub(crate) fn from_tokenizer(tokenizer: SentencePieceBPE) -> Self {
|
|
46
|
+
Self {
|
|
47
|
+
tokenizer: RefCell::new(tokenizer),
|
|
48
|
+
state: RefCell::new(EncodeState::new()),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn encode(ruby: &Ruby, rb_self: &Self, input: RString) -> Result<Vec<u32>, Error> {
|
|
53
|
+
// SAFETY: read-only, for the duration of this synchronous call.
|
|
54
|
+
let bytes = unsafe { input.as_slice() };
|
|
55
|
+
let text = require_utf8(ruby, bytes)?;
|
|
56
|
+
let mut ids: Vec<u32> = Vec::new();
|
|
57
|
+
let mut state = rb_self.state.borrow_mut();
|
|
58
|
+
rb_self.tokenizer.borrow().encode_raw_cb(&mut state, text, &mut |tokens| {
|
|
59
|
+
ids.extend(tokens.iter().map(|&t| u32::from(t)))
|
|
60
|
+
});
|
|
61
|
+
Ok(ids)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Encode a batch on the core's internal (rayon) parallelism, with the
|
|
65
|
+
/// GVL released — like `BPETokenizer#encode_batch`, always parallel
|
|
66
|
+
/// (only `encode_files` exposes a `parallel:` toggle). Every input
|
|
67
|
+
/// string is validated as UTF-8 and copied into an owned `String`
|
|
68
|
+
/// before release: nothing Ruby-managed may be touched once the GVL is
|
|
69
|
+
/// gone.
|
|
70
|
+
fn encode_batch_ragged(ruby: &Ruby, rb_self: &Self, inputs: RArray) -> Result<(Vec<u32>, Vec<i64>), Error> {
|
|
71
|
+
// SAFETY: values are read (checked-converted to `RString`, then
|
|
72
|
+
// validated and copied into owned buffers below) before anything
|
|
73
|
+
// else runs.
|
|
74
|
+
let docs: Vec<String> = unsafe { inputs.as_slice() }
|
|
75
|
+
.iter()
|
|
76
|
+
.map(|&v| {
|
|
77
|
+
let s = RString::try_convert(v)?;
|
|
78
|
+
let bytes = unsafe { s.as_slice() };
|
|
79
|
+
require_utf8(ruby, bytes).map(str::to_owned)
|
|
80
|
+
})
|
|
81
|
+
.collect::<Result<_, _>>()?;
|
|
82
|
+
let doc_refs: Vec<&str> = docs.iter().map(String::as_str).collect();
|
|
83
|
+
let tokenizer = rb_self.tokenizer.borrow();
|
|
84
|
+
let tokenizer: &SentencePieceBPE = &tokenizer;
|
|
85
|
+
Ok(without_gvl(|| sp_encode_docs_ragged(tokenizer, &doc_refs)))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn encode_batch(ruby: &Ruby, rb_self: &Self, inputs: RArray) -> Result<RArray, Error> {
|
|
89
|
+
let (flat, lens) = Self::encode_batch_ragged(ruby, rb_self, inputs)?;
|
|
90
|
+
ragged_result(ruby, flat, lens)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The packed analog of `encode_batch`: same encode core, but the flat
|
|
94
|
+
/// token ids are handed to Ruby as one `IO::Buffer` instead of being
|
|
95
|
+
/// re-chunked into per-document Arrays (see `tokenizer::packed_result`).
|
|
96
|
+
fn encode_batch_packed(ruby: &Ruby, rb_self: &Self, inputs: RArray) -> Result<RArray, Error> {
|
|
97
|
+
let (flat, lens) = Self::encode_batch_ragged(ruby, rb_self, inputs)?;
|
|
98
|
+
packed_result(ruby, flat, lens)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Encode every document named by `source` in parallel, with the GVL
|
|
102
|
+
/// released for the whole run — loading the files and the encode
|
|
103
|
+
/// itself. A `separator:` on a `TextFileSource` must be valid UTF-8
|
|
104
|
+
/// (checked up front, like the pyo3 binding's own separator check):
|
|
105
|
+
/// an arbitrary byte separator could cut a document mid-character and
|
|
106
|
+
/// break the SentencePiece cores' `&str` contract. `parallel: false`
|
|
107
|
+
/// loads and encodes everything on the calling thread instead, with
|
|
108
|
+
/// identical output, never touching rayon.
|
|
109
|
+
fn encode_files_ragged(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(Vec<u32>, Vec<i64>), Error> {
|
|
110
|
+
let args = scan_args::<(Value,), (), (), (), RHash, ()>(args)?;
|
|
111
|
+
let (source,) = args.required;
|
|
112
|
+
let kw = get_kwargs::<_, (), (Option<Value>,), ()>(args.keywords, &[], &["parallel"])?;
|
|
113
|
+
let (parallel,) = kw.optional;
|
|
114
|
+
let parallel = match parallel {
|
|
115
|
+
Some(v) if !v.is_nil() => bool::try_convert(v)?,
|
|
116
|
+
_ => true,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let source = sources::resolve(ruby, source)?;
|
|
120
|
+
if let DocFormat::Text { separator: Some(sep) } = &source.format {
|
|
121
|
+
if std::str::from_utf8(sep).is_err() {
|
|
122
|
+
return Err(raise(
|
|
123
|
+
ruby,
|
|
124
|
+
"the SentencePiece backend requires a separator that is valid UTF-8",
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let tokenizer = rb_self.tokenizer.borrow();
|
|
130
|
+
let tokenizer: &SentencePieceBPE = &tokenizer;
|
|
131
|
+
let encoded: std::io::Result<(Vec<u32>, Vec<i64>)> = without_gvl(|| {
|
|
132
|
+
sources::encode_files_ragged(&source, parallel, |files, format| {
|
|
133
|
+
for ®ion in files {
|
|
134
|
+
std::str::from_utf8(region).map_err(|e| {
|
|
135
|
+
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("invalid UTF-8 in file contents: {e}"))
|
|
136
|
+
})?;
|
|
137
|
+
}
|
|
138
|
+
Ok(if parallel {
|
|
139
|
+
sp_encode_files_docs(tokenizer, files, format)
|
|
140
|
+
} else {
|
|
141
|
+
sp_encode_files_docs_serial(tokenizer, files, format)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
encoded.map_err(|e| raise(ruby, e.to_string()))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn encode_files(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<RArray, Error> {
|
|
149
|
+
let (flat, lens) = Self::encode_files_ragged(ruby, rb_self, args)?;
|
|
150
|
+
ragged_result(ruby, flat, lens)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// The packed analog of `encode_files`: same encode core (including the
|
|
154
|
+
/// `parallel:` nil-equals-omitted contract), but the flat token ids are
|
|
155
|
+
/// handed to Ruby as one `IO::Buffer` instead of being re-chunked into
|
|
156
|
+
/// per-document Arrays (see `tokenizer::packed_result`).
|
|
157
|
+
fn encode_files_packed(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<RArray, Error> {
|
|
158
|
+
let (flat, lens) = Self::encode_files_ragged(ruby, rb_self, args)?;
|
|
159
|
+
packed_result(ruby, flat, lens)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn decode(ruby: &Ruby, rb_self: &Self, tokens: RArray) -> Result<RString, Error> {
|
|
163
|
+
let ids: Vec<u32> = tokens.to_vec()?;
|
|
164
|
+
let ids: Vec<_> = ids.into_iter().map(Into::into).collect();
|
|
165
|
+
let bytes = rb_self.tokenizer.borrow().decode(&ids);
|
|
166
|
+
Ok(binary_string(ruby, &bytes))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn vocab_size(&self) -> usize {
|
|
170
|
+
self.tokenizer.borrow().vocab_size()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn vocab(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
|
|
174
|
+
let tokenizer = rb_self.tokenizer.borrow();
|
|
175
|
+
let hash = ruby.hash_new();
|
|
176
|
+
for (id, bytes) in tokenizer.vocab_entries() {
|
|
177
|
+
hash.aset(id, binary_string(ruby, bytes))?;
|
|
178
|
+
}
|
|
179
|
+
Ok(hash)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn merges(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
183
|
+
let tokenizer = rb_self.tokenizer.borrow();
|
|
184
|
+
let entries = tokenizer.merge_entries();
|
|
185
|
+
let result = ruby.ary_new_capa(entries.len());
|
|
186
|
+
for (a, b) in entries {
|
|
187
|
+
result.push((binary_string(ruby, a), binary_string(ruby, b)))?;
|
|
188
|
+
}
|
|
189
|
+
Ok(result)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
pub fn init(ruby: &Ruby, native: RModule) -> Result<(), Error> {
|
|
194
|
+
let class: RClass = native.define_class("SentencePieceTokenizer", ruby.class_object())?;
|
|
195
|
+
class.define_method("encode", method!(SentencePieceTokenizer::encode, 1))?;
|
|
196
|
+
class.define_method("encode_batch", method!(SentencePieceTokenizer::encode_batch, 1))?;
|
|
197
|
+
class.define_method("encode_batch_packed", method!(SentencePieceTokenizer::encode_batch_packed, 1))?;
|
|
198
|
+
class.define_method("encode_files", method!(SentencePieceTokenizer::encode_files, -1))?;
|
|
199
|
+
class.define_method("encode_files_packed", method!(SentencePieceTokenizer::encode_files_packed, -1))?;
|
|
200
|
+
class.define_method("decode", method!(SentencePieceTokenizer::decode, 1))?;
|
|
201
|
+
class.define_method("vocab_size", method!(SentencePieceTokenizer::vocab_size, 0))?;
|
|
202
|
+
class.define_method("vocab", method!(SentencePieceTokenizer::vocab, 0))?;
|
|
203
|
+
class.define_method("merges", method!(SentencePieceTokenizer::merges, 0))?;
|
|
204
|
+
Ok(())
|
|
205
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
//! `Gigatoken::Native::{Text,Jsonl,Parquet}FileSource`: paths plus how to
|
|
2
|
+
//! split their bytes into documents, for `BPETokenizer#encode_files`.
|
|
3
|
+
//! Mirrors the pyo3 `FileSource` family (`src/bindings/sources.rs`) —
|
|
4
|
+
//! same constructor options, same `DocFormat` payload — but each format
|
|
5
|
+
//! gets its own Ruby class instead of a `FileSource` base class, since
|
|
6
|
+
//! magnus ties one wrapped Rust type to exactly one Ruby class.
|
|
7
|
+
//!
|
|
8
|
+
//! `encode_files_ragged` below mirrors the pyo3 path's loading scaffold
|
|
9
|
+
//! (`src/bindings/sources.rs::encode_files_ragged`), now that the crate root
|
|
10
|
+
//! re-exports `batch::encode_files_docs`/`_serial` (`src/lib.rs`): parquet
|
|
11
|
+
//! rows can't be split out of raw file bytes, so they're read directly as
|
|
12
|
+
//! owned documents via `input::parquet::read_docs` and encoded through the
|
|
13
|
+
//! whole-document path; text/JSONL files are loaded whole (mmapped, or
|
|
14
|
+
//! decompressed into memory for .gz/.zst — `load_file` handles both) and
|
|
15
|
+
//! handed straight to the fused chunk+extract+encode core as byte regions,
|
|
16
|
+
//! with no separate single-threaded document-splitting pass.
|
|
17
|
+
|
|
18
|
+
use std::path::PathBuf;
|
|
19
|
+
|
|
20
|
+
use gigatoken_rs::input::file_source::{DocFormat, LoadedFile, load_file};
|
|
21
|
+
use gigatoken_rs::input::parquet;
|
|
22
|
+
use magnus::{
|
|
23
|
+
prelude::*,
|
|
24
|
+
scan_args::{get_kwargs, scan_args},
|
|
25
|
+
Error, RClass, RHash, RModule, RString, Ruby, TryConvert, Value,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
use crate::error::raise;
|
|
29
|
+
|
|
30
|
+
/// Paths plus how to split their bytes into documents — the payload shared
|
|
31
|
+
/// by the three FileSource classes below.
|
|
32
|
+
#[derive(Clone)]
|
|
33
|
+
pub(crate) struct FileSource {
|
|
34
|
+
pub(crate) paths: Vec<PathBuf>,
|
|
35
|
+
pub(crate) format: DocFormat,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn rstring_bytes(s: RString) -> Vec<u8> {
|
|
39
|
+
// SAFETY: read-only, for the duration of this synchronous call.
|
|
40
|
+
unsafe { s.as_slice() }.to_vec()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Plain-text files. With `separator`, documents are the pieces between
|
|
44
|
+
/// separator occurrences; without one, each file is a single document.
|
|
45
|
+
#[magnus::wrap(class = "Gigatoken::Native::TextFileSource", free_immediately, size)]
|
|
46
|
+
pub struct TextFileSource(pub(crate) FileSource);
|
|
47
|
+
|
|
48
|
+
impl TextFileSource {
|
|
49
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
50
|
+
let args = scan_args::<(Vec<PathBuf>,), (), (), (), RHash, ()>(args)?;
|
|
51
|
+
let (paths,) = args.required;
|
|
52
|
+
let kw = get_kwargs::<_, (), (Option<Option<RString>>,), ()>(args.keywords, &[], &["separator"])?;
|
|
53
|
+
let (separator,) = kw.optional;
|
|
54
|
+
let separator = separator.flatten();
|
|
55
|
+
Ok(Self(FileSource {
|
|
56
|
+
paths,
|
|
57
|
+
format: DocFormat::Text {
|
|
58
|
+
separator: separator.map(rstring_bytes),
|
|
59
|
+
},
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// JSON Lines files: one document per line, text taken from `field`
|
|
65
|
+
/// (default `"text"`).
|
|
66
|
+
#[magnus::wrap(class = "Gigatoken::Native::JsonlFileSource", free_immediately, size)]
|
|
67
|
+
pub struct JsonlFileSource(pub(crate) FileSource);
|
|
68
|
+
|
|
69
|
+
impl JsonlFileSource {
|
|
70
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
71
|
+
let args = scan_args::<(Vec<PathBuf>,), (), (), (), RHash, ()>(args)?;
|
|
72
|
+
let (paths,) = args.required;
|
|
73
|
+
let kw = get_kwargs::<_, (), (Option<Option<String>>,), ()>(args.keywords, &[], &["field"])?;
|
|
74
|
+
let (field,) = kw.optional;
|
|
75
|
+
let field = field.flatten();
|
|
76
|
+
Ok(Self(FileSource {
|
|
77
|
+
paths,
|
|
78
|
+
format: DocFormat::Jsonl {
|
|
79
|
+
field: field.unwrap_or_else(|| "text".to_string()),
|
|
80
|
+
},
|
|
81
|
+
}))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Parquet files: one document per row, text taken from `column` (default
|
|
86
|
+
/// `"text"`); null rows become empty documents.
|
|
87
|
+
#[magnus::wrap(class = "Gigatoken::Native::ParquetFileSource", free_immediately, size)]
|
|
88
|
+
pub struct ParquetFileSource(pub(crate) FileSource);
|
|
89
|
+
|
|
90
|
+
impl ParquetFileSource {
|
|
91
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
92
|
+
let args = scan_args::<(Vec<PathBuf>,), (), (), (), RHash, ()>(args)?;
|
|
93
|
+
let (paths,) = args.required;
|
|
94
|
+
let kw = get_kwargs::<_, (), (Option<Option<String>>,), ()>(args.keywords, &[], &["column"])?;
|
|
95
|
+
let (column,) = kw.optional;
|
|
96
|
+
let column = column.flatten();
|
|
97
|
+
Ok(Self(FileSource {
|
|
98
|
+
paths,
|
|
99
|
+
format: DocFormat::Parquet {
|
|
100
|
+
column: column.unwrap_or_else(|| "text".to_string()),
|
|
101
|
+
},
|
|
102
|
+
}))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Resolve an `encode_files` argument to its `FileSource`: a
|
|
107
|
+
/// `TextFileSource`, `JsonlFileSource`, or `ParquetFileSource`. Bare paths
|
|
108
|
+
/// are wrapped into a `TextFileSource` on the Ruby side
|
|
109
|
+
/// (`Gigatoken::Tokenizer#encode_files`), so this only needs to handle the
|
|
110
|
+
/// three native classes.
|
|
111
|
+
pub(crate) fn resolve(ruby: &Ruby, source: Value) -> Result<FileSource, Error> {
|
|
112
|
+
if let Ok(s) = <&TextFileSource>::try_convert(source) {
|
|
113
|
+
return Ok(s.0.clone());
|
|
114
|
+
}
|
|
115
|
+
if let Ok(s) = <&JsonlFileSource>::try_convert(source) {
|
|
116
|
+
return Ok(s.0.clone());
|
|
117
|
+
}
|
|
118
|
+
if let Ok(s) = <&ParquetFileSource>::try_convert(source) {
|
|
119
|
+
return Ok(s.0.clone());
|
|
120
|
+
}
|
|
121
|
+
Err(raise(
|
|
122
|
+
ruby,
|
|
123
|
+
format!(
|
|
124
|
+
"expected a TextFileSource, JsonlFileSource, or ParquetFileSource, got {}",
|
|
125
|
+
source.class()
|
|
126
|
+
),
|
|
127
|
+
))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Load every document named by `source`, in file/row order, and hand its
|
|
131
|
+
/// contents and document format to `encode` — the fused chunk+extract+encode
|
|
132
|
+
/// core (`batch::encode_files_docs`/`_serial`, called by `tokenizer.rs`).
|
|
133
|
+
/// Parquet rows can't be split out of raw file bytes, so they are
|
|
134
|
+
/// materialized as owned documents here and encoded through the
|
|
135
|
+
/// whole-document path (each buffer one document); other formats are loaded
|
|
136
|
+
/// whole and left for `encode` to split. `encode` itself returns a
|
|
137
|
+
/// `std::io::Result` (not a `magnus::Error`) since this whole call runs with
|
|
138
|
+
/// the GVL released — the SentencePiece backend's UTF-8 validation surfaces
|
|
139
|
+
/// its failure this way too, converted to `Gigatoken::Error` only after
|
|
140
|
+
/// `without_gvl` returns (see `sentencepiece.rs`).
|
|
141
|
+
pub(crate) fn encode_files_ragged(
|
|
142
|
+
source: &FileSource,
|
|
143
|
+
parallel: bool,
|
|
144
|
+
encode: impl FnOnce(&[&[u8]], &DocFormat) -> std::io::Result<(Vec<u32>, Vec<i64>)>,
|
|
145
|
+
) -> std::io::Result<(Vec<u32>, Vec<i64>)> {
|
|
146
|
+
if let DocFormat::Parquet { column } = &source.format {
|
|
147
|
+
let docs = load_parquet_docs(&source.paths, column, parallel)?;
|
|
148
|
+
let bytes: Vec<&[u8]> = docs.iter().map(Vec::as_slice).collect();
|
|
149
|
+
return encode(&bytes, &DocFormat::Text { separator: None });
|
|
150
|
+
}
|
|
151
|
+
let files = load_files(&source.paths, parallel)?;
|
|
152
|
+
let bytes: Vec<&[u8]> = files.iter().map(LoadedFile::as_bytes).collect();
|
|
153
|
+
encode(&bytes, &source.format)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Load `column` of every parquet file as one owned document per row, files
|
|
157
|
+
/// in argument order, rows in row order. Parallel across files and row
|
|
158
|
+
/// groups with rayon, or fully on the calling thread when `parallel` is
|
|
159
|
+
/// false (the sequential encode paths must never touch the rayon pool).
|
|
160
|
+
fn load_parquet_docs(
|
|
161
|
+
paths: &[PathBuf],
|
|
162
|
+
column: &str,
|
|
163
|
+
parallel: bool,
|
|
164
|
+
) -> std::io::Result<Vec<Vec<u8>>> {
|
|
165
|
+
use rayon::prelude::*;
|
|
166
|
+
let per_file: Vec<Vec<Vec<u8>>> = if parallel {
|
|
167
|
+
paths
|
|
168
|
+
.par_iter()
|
|
169
|
+
.map(|p| parquet::read_docs(p, column, true))
|
|
170
|
+
.collect::<std::io::Result<_>>()?
|
|
171
|
+
} else {
|
|
172
|
+
paths
|
|
173
|
+
.iter()
|
|
174
|
+
.map(|p| parquet::read_docs(p, column, false))
|
|
175
|
+
.collect::<std::io::Result<_>>()?
|
|
176
|
+
};
|
|
177
|
+
Ok(per_file.into_iter().flatten().collect())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Load all files: mmap when stored uncompressed, decompress .gz/.zst into
|
|
181
|
+
/// memory otherwise (parallel chunking needs random access). In parallel
|
|
182
|
+
/// with rayon, or serially on the calling thread when `parallel` is false
|
|
183
|
+
/// (the sequential encode paths must never touch the rayon pool).
|
|
184
|
+
fn load_files(paths: &[PathBuf], parallel: bool) -> std::io::Result<Vec<LoadedFile>> {
|
|
185
|
+
use rayon::prelude::*;
|
|
186
|
+
let load = |p: &PathBuf| {
|
|
187
|
+
load_file(p).map_err(|e| std::io::Error::new(e.kind(), format!("{}: {e}", p.display())))
|
|
188
|
+
};
|
|
189
|
+
if parallel {
|
|
190
|
+
paths.par_iter().map(load).collect()
|
|
191
|
+
} else {
|
|
192
|
+
paths.iter().map(load).collect()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pub fn init(ruby: &Ruby, native: RModule) -> Result<(), Error> {
|
|
197
|
+
let text: RClass = native.define_class("TextFileSource", ruby.class_object())?;
|
|
198
|
+
text.define_singleton_method("new", magnus::function!(TextFileSource::new, -1))?;
|
|
199
|
+
|
|
200
|
+
let jsonl: RClass = native.define_class("JsonlFileSource", ruby.class_object())?;
|
|
201
|
+
jsonl.define_singleton_method("new", magnus::function!(JsonlFileSource::new, -1))?;
|
|
202
|
+
|
|
203
|
+
let parquet: RClass = native.define_class("ParquetFileSource", ruby.class_object())?;
|
|
204
|
+
parquet.define_singleton_method("new", magnus::function!(ParquetFileSource::new, -1))?;
|
|
205
|
+
|
|
206
|
+
Ok(())
|
|
207
|
+
}
|