gigatoken 0.2.2 → 0.4.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 +1 -1
- data/Cargo.toml +7 -3
- data/README.md +22 -5
- data/benches/common/mod.rs +105 -0
- data/benches/encode.rs +79 -0
- data/benches/encode_doc.rs +60 -0
- data/benches/encode_st.rs +105 -0
- data/benches/encode_st_sp.rs +51 -0
- data/benches/pretokenize.rs +151 -0
- data/benches/pretokenize_profile.rs +36 -0
- data/benches/pretokenize_profile_all.rs +52 -0
- data/benches/simdutf_transcode.rs +82 -0
- data/benches/unicode.rs +88 -0
- data/ext/gigatoken/Cargo.toml +1 -1
- data/ext/gigatoken/src/error.rs +29 -9
- data/ext/gigatoken/src/gvl.rs +170 -36
- data/ext/gigatoken/src/lib.rs +18 -3
- data/ext/gigatoken/src/sentencepiece.rs +27 -15
- data/ext/gigatoken/src/tokenizer.rs +225 -131
- data/lib/gigatoken/cli/bench.rb +10 -4
- data/lib/gigatoken/cli/support.rb +40 -8
- data/lib/gigatoken/cli/validate.rb +2 -1
- data/lib/gigatoken/encodings.rb +9 -4
- data/lib/gigatoken/hub.rb +240 -38
- data/lib/gigatoken/packed_result.rb +12 -4
- data/lib/gigatoken/tokenizer.rb +85 -15
- data/lib/gigatoken/version.rb +1 -1
- data/lib/gigatoken.rb +17 -1
- data/rust-toolchain.toml +1 -1
- data/src/batch.rs +294 -56
- data/src/bpe/tiktoken.rs +30 -5
- metadata +25 -1
data/ext/gigatoken/src/gvl.rs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
//! (`~/architect/src/github.com/socketry/io-event/ext/io/event/worker_pool.c:309-316`).
|
|
18
18
|
//! Without such a scheduler (or with one lacking a worker pool), `rb_nogvl`
|
|
19
19
|
//! degrades to exactly today's behavior: release the GVL, block this thread.
|
|
20
|
-
//! See `docs/
|
|
20
|
+
//! See `docs/explanation/async-design.md` and `docs/how-to/run-under-async.md` for the full design
|
|
21
21
|
//! and gotchas (worker pool is opt-in, defaults to one background worker).
|
|
22
22
|
//!
|
|
23
23
|
//! `func`/`data1` may now run on a different OS thread than the caller (the
|
|
@@ -36,15 +36,27 @@
|
|
|
36
36
|
//! `VALUE` or thread-local state, so running one OS thread over instead of
|
|
37
37
|
//! another changes nothing about its safety, and the types involved satisfy
|
|
38
38
|
//! `Send`/`Sync` on their own merits.
|
|
39
|
+
//!
|
|
40
|
+
//! Interrupts. `rb_nogvl` ends by checking interrupts and raises any that
|
|
41
|
+
//! are pending — a `Timeout`, `Thread#kill`, `Interrupt`, an `Async`
|
|
42
|
+
//! timeout — by longjmping out of itself, over every Rust frame in between.
|
|
43
|
+
//! Both entry points here therefore call it under `magnus::rb_sys::protect`
|
|
44
|
+
//! (see [`run`]), so the raise comes back as an ordinary `Error` and every
|
|
45
|
+
//! guard, `InputDocs` and Ruby String lock the caller holds is released by
|
|
46
|
+
//! ordinary Rust unwinding on the way out. [`without_gvl_cancellable`] goes
|
|
47
|
+
//! one further and supplies a real unblock function, so a pending interrupt
|
|
48
|
+
//! cancels the work in flight instead of waiting for it.
|
|
39
49
|
|
|
40
50
|
use std::any::Any;
|
|
41
51
|
use std::ffi::c_void;
|
|
42
52
|
use std::os::raw::c_int;
|
|
43
53
|
use std::panic::{self, AssertUnwindSafe};
|
|
54
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
44
55
|
|
|
56
|
+
use magnus::Error;
|
|
45
57
|
use rb_sys::rb_nogvl;
|
|
46
58
|
|
|
47
|
-
/// `RB_NOGVL_OFFLOAD_SAFE` (`ruby/thread.h:
|
|
59
|
+
/// `RB_NOGVL_OFFLOAD_SAFE` (`ruby/thread.h:73` in Ruby 4.0.7's own headers;
|
|
48
60
|
/// introduced by Ruby's `Fiber::Scheduler#blocking_operation_wait` support,
|
|
49
61
|
/// first released in Ruby 3.4.0). Defined locally rather than taken from
|
|
50
62
|
/// `rb_sys::` bindings: `rb-sys` bindgens its constants from the *building*
|
|
@@ -61,62 +73,184 @@ const RB_NOGVL_OFFLOAD_SAFE: c_int = 0x4;
|
|
|
61
73
|
/// value, or a caught panic payload to re-raise once we're back on ordinary
|
|
62
74
|
/// (non-`extern "C"`) Rust stack frames. Unwinding a panic directly across
|
|
63
75
|
/// the `extern "C"` trampoline `rb_nogvl` calls into is undefined behavior;
|
|
64
|
-
/// catching it here and resuming it from `
|
|
65
|
-
/// an ordinary Rust panic, which magnus's own `method!`/`function!`
|
|
66
|
-
/// trampolines already wrap in `catch_unwind` and convert into a fatal
|
|
67
|
-
/// exception (`magnus::error::Error::from_panic`) — the same outcome
|
|
68
|
-
/// other panicking native method already gets, just carried safely
|
|
69
|
-
/// the extra C boundary this one call adds.
|
|
76
|
+
/// catching it here and resuming it from [`Slot::take`] below turns that
|
|
77
|
+
/// into an ordinary Rust panic, which magnus's own `method!`/`function!`
|
|
78
|
+
/// call trampolines already wrap in `catch_unwind` and convert into a fatal
|
|
79
|
+
/// Ruby exception (`magnus::error::Error::from_panic`) — the same outcome
|
|
80
|
+
/// any other panicking native method already gets, just carried safely
|
|
81
|
+
/// across the extra C boundary this one call adds.
|
|
70
82
|
enum Outcome<R> {
|
|
71
83
|
Value(R),
|
|
72
84
|
Panic(Box<dyn Any + Send + 'static>),
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
/// The one thing `rb_nogvl`'s callback and [`run`] share: the callback takes
|
|
88
|
+
/// the closure out of `input` and leaves its result in `output`. The rule is
|
|
89
|
+
/// that nothing travels through `rb_nogvl`'s *return value* — the caller owns
|
|
90
|
+
/// this slot, in an ordinary Rust frame.
|
|
91
|
+
///
|
|
92
|
+
/// That rule is what makes the raise `rb_nogvl` performs on its way out
|
|
93
|
+
/// leak-free. The `protect` closure in [`run`] holds the single FFI call and
|
|
94
|
+
/// owns nothing at all, so the longjmp skips no destructor that matters:
|
|
95
|
+
/// whatever is in flight is either here — in a frame that unwinds normally
|
|
96
|
+
/// afterwards — or in one of the caller's own frames, which unwind with it.
|
|
97
|
+
/// It is also the only way to tell "the callback ran" from "it never did",
|
|
98
|
+
/// which a fiber scheduler cancelling the offloaded operation before its
|
|
99
|
+
/// worker pool picks it up makes a real case (io-event's
|
|
100
|
+
/// `worker_pool_work_wait`, `ext/io/event/worker_pool.c`).
|
|
101
|
+
struct Slot<F, R> {
|
|
102
|
+
input: Option<F>,
|
|
103
|
+
output: Option<Outcome<R>>,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
impl<F, R> Slot<F, R> {
|
|
107
|
+
fn new(f: F) -> Self {
|
|
108
|
+
Self {
|
|
109
|
+
input: Some(f),
|
|
110
|
+
output: None,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The callback's value, or `None` if it never ran. A panic it caught
|
|
115
|
+
/// resumes here, on an ordinary Rust frame.
|
|
116
|
+
fn take(&mut self) -> Option<R> {
|
|
117
|
+
match self.output.take() {
|
|
118
|
+
Some(Outcome::Value(value)) => Some(value),
|
|
119
|
+
Some(Outcome::Panic(payload)) => panic::resume_unwind(payload),
|
|
120
|
+
None => None,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
75
125
|
unsafe extern "C" fn call_without_gvl<F, R>(arg: *mut c_void) -> *mut c_void
|
|
76
126
|
where
|
|
77
127
|
F: FnOnce() -> R + Send,
|
|
78
128
|
R: Send,
|
|
79
129
|
{
|
|
80
|
-
// SAFETY: `arg` is the `*mut
|
|
81
|
-
// for the duration of that (synchronous) call, and this is the
|
|
82
|
-
// place it's dereferenced.
|
|
83
|
-
let
|
|
130
|
+
// SAFETY: `arg` is the `*mut Slot<F, R>` handed to `rb_nogvl` by `run`,
|
|
131
|
+
// valid for the duration of that (synchronous) call, and this is the
|
|
132
|
+
// only place it's dereferenced.
|
|
133
|
+
let slot = unsafe { &mut *(arg as *mut Slot<F, R>) };
|
|
134
|
+
let closure = slot
|
|
135
|
+
.input
|
|
136
|
+
.take()
|
|
84
137
|
.expect("without_gvl callback invoked more than once");
|
|
85
|
-
|
|
138
|
+
slot.output = Some(match panic::catch_unwind(AssertUnwindSafe(closure)) {
|
|
86
139
|
Ok(value) => Outcome::Value(value),
|
|
87
140
|
Err(payload) => Outcome::Panic(payload),
|
|
88
|
-
};
|
|
89
|
-
|
|
141
|
+
});
|
|
142
|
+
std::ptr::null_mut()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Run `slot`'s closure with the GVL released, under `rb_protect`.
|
|
146
|
+
///
|
|
147
|
+
/// `rb_nogvl` finishes by checking interrupts and raising any that are
|
|
148
|
+
/// pending, by longjmping out of itself. `protect` catches that and returns
|
|
149
|
+
/// it as an `Error` — including the `Tag::Fatal` a `Thread#kill` jumps with,
|
|
150
|
+
/// which magnus resumes with `rb_jump_tag` once the caller's frames have
|
|
151
|
+
/// unwound. The closure below is deliberately trivial: one FFI call and a
|
|
152
|
+
/// `nil`, owning nothing the longjmp could strand.
|
|
153
|
+
fn run<F, R>(
|
|
154
|
+
slot: &mut Slot<F, R>,
|
|
155
|
+
ubf: rb_sys::rb_unblock_function_t,
|
|
156
|
+
data2: *mut c_void,
|
|
157
|
+
) -> Result<(), Error>
|
|
158
|
+
where
|
|
159
|
+
F: FnOnce() -> R + Send,
|
|
160
|
+
R: Send,
|
|
161
|
+
{
|
|
162
|
+
let arg = slot as *mut Slot<F, R> as *mut c_void;
|
|
163
|
+
let nil: rb_sys::VALUE = rb_sys::Qnil.into();
|
|
164
|
+
magnus::rb_sys::protect(|| {
|
|
165
|
+
// SAFETY: `arg` points at `slot`, which outlives this synchronous
|
|
166
|
+
// call; the callback is the only reader of it (see `Slot`).
|
|
167
|
+
unsafe {
|
|
168
|
+
rb_nogvl(
|
|
169
|
+
Some(call_without_gvl::<F, R>),
|
|
170
|
+
arg,
|
|
171
|
+
ubf,
|
|
172
|
+
data2,
|
|
173
|
+
RB_NOGVL_OFFLOAD_SAFE,
|
|
174
|
+
)
|
|
175
|
+
};
|
|
176
|
+
nil
|
|
177
|
+
})?;
|
|
178
|
+
Ok(())
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// `rb_nogvl`'s unblock function: Ruby calls it from another thread when an
|
|
182
|
+
/// interrupt is pending for this one, and a fiber scheduler calls it to
|
|
183
|
+
/// cancel an offloaded operation (`rb_fiber_scheduler_blocking_operation_cancel`
|
|
184
|
+
/// "marks it as cancelled and calls the unblock function", Ruby 4.0.7's
|
|
185
|
+
/// `ruby/fiber/scheduler.h:455-457` — which is how an `Async` timeout reaches
|
|
186
|
+
/// an encode running on io-event's worker pool). It does exactly one thing:
|
|
187
|
+
/// set the flag the core's encode loops poll (`src/batch.rs`), so the encode
|
|
188
|
+
/// stops at the next document boundary instead of running to completion.
|
|
189
|
+
unsafe extern "C" fn set_cancel(arg: *mut c_void) {
|
|
190
|
+
// SAFETY: `arg` is the `&AtomicBool` `without_gvl_cancellable` passed as
|
|
191
|
+
// `data2`, living in its frame for the whole `rb_nogvl` call — the only
|
|
192
|
+
// window in which Ruby may call this.
|
|
193
|
+
unsafe { &*(arg as *const AtomicBool) }.store(true, Ordering::Relaxed);
|
|
90
194
|
}
|
|
91
195
|
|
|
92
196
|
/// Run `f` with the GVL released: other Ruby threads may run while `f`
|
|
93
197
|
/// executes, and — under a fiber scheduler with a worker pool — the calling
|
|
94
198
|
/// fiber yields to the reactor while `f` runs on a background thread. `f`
|
|
95
199
|
/// must not touch any Ruby object (`VALUE`) — only plain Rust data — per the
|
|
96
|
-
/// Ruby C API's contract for this call.
|
|
97
|
-
///
|
|
98
|
-
|
|
200
|
+
/// Ruby C API's contract for this call. An interrupt that arrives meanwhile
|
|
201
|
+
/// is delivered once `f` has finished, as an `Err`; use
|
|
202
|
+
/// [`without_gvl_cancellable`] for work that can stop early.
|
|
203
|
+
pub fn without_gvl<F, R>(f: F) -> Result<R, Error>
|
|
99
204
|
where
|
|
100
205
|
F: FnOnce() -> R + Send,
|
|
101
206
|
R: Send,
|
|
102
207
|
{
|
|
103
|
-
let mut slot =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
208
|
+
let mut slot = Slot::new(f);
|
|
209
|
+
run(&mut slot, None, std::ptr::null_mut())?;
|
|
210
|
+
Ok(match slot.take() {
|
|
211
|
+
Some(value) => value,
|
|
212
|
+
// A fiber scheduler cancelled the offloaded operation before its
|
|
213
|
+
// worker pool ever started it, and nothing was raised (that would
|
|
214
|
+
// have come back as an `Err` above). The closure is still in the
|
|
215
|
+
// slot and still ours to run: do that here, GVL and all, rather
|
|
216
|
+
// than invent a result.
|
|
217
|
+
None => slot.input.take().expect("the callback left the closure")(),
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// [`without_gvl`] for work that can stop early: `attempt` builds the
|
|
222
|
+
/// closure, which gets a cancellation token to poll and returns `None` if it
|
|
223
|
+
/// saw the token set and cut its work short. An interrupt arriving during
|
|
224
|
+
/// the run sets that token through [`set_cancel`], so the caller sees the
|
|
225
|
+
/// interrupt instead of waiting out the whole batch.
|
|
226
|
+
///
|
|
227
|
+
/// `attempt` is a factory, not the closure itself, because a cancelled run
|
|
228
|
+
/// sometimes has to be redone: Ruby calls the unblock function for every
|
|
229
|
+
/// interrupt, including ones that never raise (a trap handler, a
|
|
230
|
+
/// `Thread#wakeup`), and returning the half-encoded batch those produce
|
|
231
|
+
/// would be a silently truncated result. The redo runs uninterruptibly —
|
|
232
|
+
/// plain [`without_gvl`], which is exactly how this path behaved before
|
|
233
|
+
/// cancellation existed — so a chatty signal handler can cost a batch one
|
|
234
|
+
/// extra pass, never an unbounded number of them.
|
|
235
|
+
pub fn without_gvl_cancellable<A, F, R>(mut attempt: A) -> Result<R, Error>
|
|
236
|
+
where
|
|
237
|
+
A: FnMut() -> F,
|
|
238
|
+
F: FnOnce(&AtomicBool) -> Option<R> + Send,
|
|
239
|
+
R: Send,
|
|
240
|
+
{
|
|
241
|
+
let cancel = AtomicBool::new(false);
|
|
242
|
+
let f = attempt();
|
|
243
|
+
let mut slot = Slot::new(|| f(&cancel));
|
|
244
|
+
run(
|
|
245
|
+
&mut slot,
|
|
246
|
+
Some(set_cancel),
|
|
247
|
+
&cancel as *const AtomicBool as *mut c_void,
|
|
248
|
+
)?;
|
|
249
|
+
if let Some(Some(value)) = slot.take() {
|
|
250
|
+
return Ok(value);
|
|
121
251
|
}
|
|
252
|
+
let f = attempt();
|
|
253
|
+
let never_cancelled = AtomicBool::new(false);
|
|
254
|
+
without_gvl(|| f(&never_cancelled))
|
|
255
|
+
.map(|value| value.expect("nothing sets the token of an uncancellable run"))
|
|
122
256
|
}
|
data/ext/gigatoken/src/lib.rs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use gigatoken_rs::input::file_source::load_file;
|
|
1
2
|
use gigatoken_rs::load_tokenizer::hf::{self, HfTokenizer};
|
|
2
3
|
use gigatoken_rs::pretokenize::PretokenizerType;
|
|
3
4
|
use magnus::{Error, Module, RString, Ruby, Value, function};
|
|
@@ -26,9 +27,9 @@ mod sentencepiece;
|
|
|
26
27
|
mod sources;
|
|
27
28
|
mod tokenizer;
|
|
28
29
|
|
|
29
|
-
use error::raise;
|
|
30
|
+
use error::{model_error, raise};
|
|
30
31
|
use sentencepiece::SentencePieceTokenizer;
|
|
31
|
-
use tokenizer::BPETokenizer;
|
|
32
|
+
use tokenizer::{binary_string, BPETokenizer};
|
|
32
33
|
|
|
33
34
|
// The gigatoken core crate exposes no version constant of its own, so this
|
|
34
35
|
// is the ext crate's (gigatoken-rb's) version — see the builder report.
|
|
@@ -66,7 +67,20 @@ fn load_hf_json(ruby: &Ruby, data: RString) -> Result<Value, Error> {
|
|
|
66
67
|
match hf::load_hf_slice(bytes) {
|
|
67
68
|
Ok(HfTokenizer::Bpe(tokenizer)) => Ok(ruby.into_value(BPETokenizer::from_tokenizer(tokenizer))),
|
|
68
69
|
Ok(HfTokenizer::SentencePiece(tokenizer)) => Ok(ruby.into_value(SentencePieceTokenizer::from_tokenizer(tokenizer))),
|
|
69
|
-
Err(e) => Err(
|
|
70
|
+
Err(e) => Err(model_error(ruby, e.to_string())),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// One file's contents as a binary String, decompressed by extension the way
|
|
75
|
+
/// the native file sources do it (`.gz`, `.zst`/`.zstd`, plain — see the core's
|
|
76
|
+
/// `load_file`). The CLI's Ruby-side split reads through here so both sides of
|
|
77
|
+
/// `gigatoken validate` see the same bytes with one decoder between them
|
|
78
|
+
/// (`lib/gigatoken/cli/support.rb`); the encode paths never touch it, which is
|
|
79
|
+
/// why it keeps the GVL.
|
|
80
|
+
fn read_input(ruby: &Ruby, path: String) -> Result<RString, Error> {
|
|
81
|
+
match load_file(std::path::Path::new(&path)) {
|
|
82
|
+
Ok(file) => Ok(binary_string(ruby, file.as_bytes())),
|
|
83
|
+
Err(e) => Err(raise(ruby, format!("{path}: {e}"))),
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
|
|
@@ -77,6 +91,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
77
91
|
native.define_module_function("crate_version", function!(crate_version, 0))?;
|
|
78
92
|
native.define_module_function("load_hf_json", function!(load_hf_json, 1))?;
|
|
79
93
|
native.define_module_function("pretokenizer_names", function!(pretokenizer_names, 0))?;
|
|
94
|
+
native.define_module_function("read_input", function!(read_input, 1))?;
|
|
80
95
|
native.define_module_function("set_max_cache_bytes", function!(set_max_cache_bytes, 1))?;
|
|
81
96
|
native.define_module_function("get_max_cache_bytes", function!(get_max_cache_bytes, 0))?;
|
|
82
97
|
sources::init(ruby, native)?;
|
|
@@ -22,14 +22,16 @@ use magnus::{
|
|
|
22
22
|
scan_args::{get_kwargs, scan_args},
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
use crate::error::raise;
|
|
25
|
+
use crate::error::{input_error, raise};
|
|
26
26
|
use crate::gvl::without_gvl;
|
|
27
27
|
use crate::sources;
|
|
28
|
-
use crate::tokenizer::{
|
|
28
|
+
use crate::tokenizer::{
|
|
29
|
+
binary_string, packed_result, ragged_result, require_known_ids, snapshot_entry, snapshot_inputs,
|
|
30
|
+
};
|
|
29
31
|
|
|
30
|
-
/// Validate that `bytes` is UTF-8, raising `Gigatoken::
|
|
32
|
+
/// Validate that `bytes` is UTF-8, raising `Gigatoken::InputError` otherwise.
|
|
31
33
|
fn require_utf8<'a>(ruby: &Ruby, bytes: &'a [u8]) -> Result<&'a str, Error> {
|
|
32
|
-
std::str::from_utf8(bytes).map_err(|e|
|
|
34
|
+
std::str::from_utf8(bytes).map_err(|e| input_error(ruby, format!("invalid UTF-8: {e}")))
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
#[magnus::wrap(class = "Gigatoken::Native::SentencePieceTokenizer", free_immediately, size)]
|
|
@@ -40,7 +42,10 @@ pub struct SentencePieceTokenizer {
|
|
|
40
42
|
tokenizer: SentencePieceBPE,
|
|
41
43
|
// The one mutable piece. A `Mutex` rather than a `RefCell` so the wrapped
|
|
42
44
|
// object is `Sync`: Ruby hands the same instance to every thread, and a
|
|
43
|
-
// `RefCell` shared that way is unsound
|
|
45
|
+
// `RefCell` shared that way is unsound. It is only ever taken with the
|
|
46
|
+
// GVL held — the batch paths build their own per-call encoders — so it
|
|
47
|
+
// cannot actually block, which is what `BPETokenizer`'s invariant asks
|
|
48
|
+
// of any lock on this side of the boundary.
|
|
44
49
|
state: Mutex<EncodeState>,
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -74,21 +79,27 @@ impl SentencePieceTokenizer {
|
|
|
74
79
|
/// string is validated as UTF-8 and copied into an owned `String`
|
|
75
80
|
/// before release: nothing Ruby-managed may be touched once the GVL is
|
|
76
81
|
/// gone.
|
|
82
|
+
///
|
|
83
|
+
/// The walk reads a [`snapshot_inputs`] copy of the caller's array, one
|
|
84
|
+
/// slot at a time — never a Rust borrow of the array's buffer held
|
|
85
|
+
/// across `try_convert`, which runs `to_str` and with it arbitrary Ruby
|
|
86
|
+
/// code, including code that re-homes the array out from under the
|
|
87
|
+
/// walk. Same contract as the BPE path: the result reflects the array
|
|
88
|
+
/// as it was at entry.
|
|
77
89
|
fn encode_batch_ragged(ruby: &Ruby, rb_self: &Self, inputs: RArray) -> Result<(Vec<u32>, Vec<i64>), Error> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let s = RString::try_convert(v)?;
|
|
90
|
+
let snapshot = snapshot_inputs(inputs);
|
|
91
|
+
let docs: Vec<String> = (0..snapshot.len())
|
|
92
|
+
.map(|i| {
|
|
93
|
+
let s = RString::try_convert(snapshot_entry(snapshot, i))?;
|
|
94
|
+
// SAFETY: validated and copied into an owned buffer right
|
|
95
|
+
// here, with no Ruby call in between to invalidate it.
|
|
85
96
|
let bytes = unsafe { s.as_slice() };
|
|
86
97
|
require_utf8(ruby, bytes).map(str::to_owned)
|
|
87
98
|
})
|
|
88
99
|
.collect::<Result<_, _>>()?;
|
|
89
100
|
let doc_refs: Vec<&str> = docs.iter().map(String::as_str).collect();
|
|
90
101
|
let tokenizer: &SentencePieceBPE = &rb_self.tokenizer;
|
|
91
|
-
|
|
102
|
+
without_gvl(|| sp_encode_docs_ragged(tokenizer, &doc_refs))
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
fn encode_batch(ruby: &Ruby, rb_self: &Self, inputs: RArray) -> Result<RArray, Error> {
|
|
@@ -133,7 +144,7 @@ impl SentencePieceTokenizer {
|
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
let tokenizer: &SentencePieceBPE = &rb_self.tokenizer;
|
|
136
|
-
let encoded
|
|
147
|
+
let encoded = without_gvl(|| {
|
|
137
148
|
sources::encode_files_ragged(&source, parallel, |files, format| {
|
|
138
149
|
for ®ion in files {
|
|
139
150
|
std::str::from_utf8(region).map_err(|e| {
|
|
@@ -146,7 +157,7 @@ impl SentencePieceTokenizer {
|
|
|
146
157
|
sp_encode_files_docs_serial(tokenizer, files, format)
|
|
147
158
|
})
|
|
148
159
|
})
|
|
149
|
-
})
|
|
160
|
+
})?;
|
|
150
161
|
encoded.map_err(|e| raise(ruby, e.to_string()))
|
|
151
162
|
}
|
|
152
163
|
|
|
@@ -166,6 +177,7 @@ impl SentencePieceTokenizer {
|
|
|
166
177
|
|
|
167
178
|
fn decode(ruby: &Ruby, rb_self: &Self, tokens: RArray) -> Result<RString, Error> {
|
|
168
179
|
let ids: Vec<u32> = tokens.to_vec()?;
|
|
180
|
+
require_known_ids(ruby, &ids, rb_self.tokenizer.vocab_size())?;
|
|
169
181
|
let ids: Vec<_> = ids.into_iter().map(Into::into).collect();
|
|
170
182
|
let bytes = rb_self.tokenizer.decode(&ids);
|
|
171
183
|
Ok(binary_string(ruby, &bytes))
|