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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +3016 -0
  3. data/Cargo.toml +135 -0
  4. data/LICENSE +21 -0
  5. data/README.md +141 -0
  6. data/exe/gigatoken +9 -0
  7. data/ext/gigatoken/Cargo.toml +24 -0
  8. data/ext/gigatoken/extconf.rb +19 -0
  9. data/ext/gigatoken/src/error.rs +20 -0
  10. data/ext/gigatoken/src/gvl.rs +122 -0
  11. data/ext/gigatoken/src/lib.rs +50 -0
  12. data/ext/gigatoken/src/sentencepiece.rs +205 -0
  13. data/ext/gigatoken/src/sources.rs +207 -0
  14. data/ext/gigatoken/src/tokenizer.rs +571 -0
  15. data/lib/gigatoken/cli/bench.rb +64 -0
  16. data/lib/gigatoken/cli/support.rb +132 -0
  17. data/lib/gigatoken/cli/validate.rb +55 -0
  18. data/lib/gigatoken/cli.rb +18 -0
  19. data/lib/gigatoken/hub.rb +242 -0
  20. data/lib/gigatoken/packed_result.rb +48 -0
  21. data/lib/gigatoken/tokenizer.rb +117 -0
  22. data/lib/gigatoken/version.rb +5 -0
  23. data/lib/gigatoken.rb +29 -0
  24. data/rust-toolchain.toml +8 -0
  25. data/src/batch.rs +1808 -0
  26. data/src/bindings/bridge.rs +396 -0
  27. data/src/bindings/hub.rs +42 -0
  28. data/src/bindings/matcher.rs +114 -0
  29. data/src/bindings/mod.rs +14 -0
  30. data/src/bindings/padding.rs +177 -0
  31. data/src/bindings/pretokenize.rs +53 -0
  32. data/src/bindings/sources.rs +273 -0
  33. data/src/bindings/train.rs +125 -0
  34. data/src/bpe/mod.rs +1217 -0
  35. data/src/bpe/pretoken_cache.rs +495 -0
  36. data/src/bpe/sentencepiece.rs +1485 -0
  37. data/src/bpe/tiktoken.rs +2555 -0
  38. data/src/bpe_train.rs +351 -0
  39. data/src/input/decompress.rs +11 -0
  40. data/src/input/file_source.rs +514 -0
  41. data/src/input/jsonl.rs +94 -0
  42. data/src/input/mod.rs +333 -0
  43. data/src/input/parquet.rs +303 -0
  44. data/src/lib.rs +578 -0
  45. data/src/load_tokenizer/hf.rs +1036 -0
  46. data/src/load_tokenizer/hub.rs +344 -0
  47. data/src/load_tokenizer/mod.rs +3 -0
  48. data/src/load_tokenizer/tiktoken.rs +87 -0
  49. data/src/main.rs +95 -0
  50. data/src/pretokenize/fast/cl100k.rs +426 -0
  51. data/src/pretokenize/fast/cl100k_family.rs +891 -0
  52. data/src/pretokenize/fast/deepseek_v3.rs +605 -0
  53. data/src/pretokenize/fast/kimi.rs +281 -0
  54. data/src/pretokenize/fast/mask.rs +1486 -0
  55. data/src/pretokenize/fast/mod.rs +446 -0
  56. data/src/pretokenize/fast/nemotron.rs +138 -0
  57. data/src/pretokenize/fast/o200k.rs +347 -0
  58. data/src/pretokenize/fast/o200k_family.rs +1734 -0
  59. data/src/pretokenize/fast/olmo3.rs +505 -0
  60. data/src/pretokenize/fast/qwen2.rs +429 -0
  61. data/src/pretokenize/fast/qwen3_5.rs +541 -0
  62. data/src/pretokenize/fast/r50k.rs +1250 -0
  63. data/src/pretokenize/mod.rs +1079 -0
  64. data/src/pretokenize/options.rs +188 -0
  65. data/src/pretokenize/pretoken.rs +20 -0
  66. data/src/pretokenize/pretokenize_traits.rs +49 -0
  67. data/src/pretokenize/reference/avx512.rs +522 -0
  68. data/src/pretokenize/reference/combinator.rs +572 -0
  69. data/src/pretokenize/reference/mod.rs +28 -0
  70. data/src/pretokenize/reference/simd.rs +852 -0
  71. data/src/pretokenize/reference/state_machine.rs +365 -0
  72. data/src/pretokenize/unicode.rs +546 -0
  73. data/src/test_hub.rs +28 -0
  74. data/src/token.rs +42 -0
  75. 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 &region 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
+ }