gigatoken 0.2.0 → 0.2.1
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/ext/gigatoken/Cargo.toml +1 -1
- data/ext/gigatoken/src/sentencepiece.rs +19 -18
- data/ext/gigatoken/src/tokenizer.rs +85 -18
- data/lib/gigatoken/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eee37e720c1b0fba0305398b731c3bd909af1a6ebc1ae7393e924c66fd629377
|
|
4
|
+
data.tar.gz: 966c52f87dcff1b0cda6218c45b0cbc5362a02655cb0c570034b4d91249a2518
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75eda453e48567dda4b39765031ed1dab4e5d12715507f0515e6742de2dd0ce67a9c4b5e4c1581664bce956b01cf8978cc9198ee3878253bd26287ba0d558aa2
|
|
7
|
+
data.tar.gz: 3a4e18c3e8603ae524cfa48752177277297c94343623571ad32bb87434eacfa3456264769f9b3daf5421ad5d615de47502607db17e6ded024e8c9b35a542b9c1
|
data/Cargo.lock
CHANGED
data/ext/gigatoken/Cargo.toml
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//! `Gigatoken::Error` instead of ever calling `str::from_utf8_unchecked` on
|
|
14
14
|
//! Ruby-supplied bytes.
|
|
15
15
|
|
|
16
|
-
use std::
|
|
16
|
+
use std::sync::Mutex;
|
|
17
17
|
|
|
18
18
|
use gigatoken_rs::input::file_source::DocFormat;
|
|
19
19
|
use gigatoken_rs::{EncodeState, SentencePieceBPE, sp_encode_docs_ragged, sp_encode_files_docs, sp_encode_files_docs_serial};
|
|
@@ -35,10 +35,13 @@ fn require_utf8<'a>(ruby: &Ruby, bytes: &'a [u8]) -> Result<&'a str, Error> {
|
|
|
35
35
|
#[magnus::wrap(class = "Gigatoken::Native::SentencePieceTokenizer", free_immediately, size)]
|
|
36
36
|
pub struct SentencePieceTokenizer {
|
|
37
37
|
// `SentencePieceBPE`'s encode methods take `&self` (only `EncodeState`
|
|
38
|
-
// is mutated), so
|
|
39
|
-
//
|
|
40
|
-
tokenizer:
|
|
41
|
-
|
|
38
|
+
// is mutated), so the model needs no interior mutability at all — every
|
|
39
|
+
// path here reads it, including the ones that release the GVL.
|
|
40
|
+
tokenizer: SentencePieceBPE,
|
|
41
|
+
// The one mutable piece. A `Mutex` rather than a `RefCell` so the wrapped
|
|
42
|
+
// object is `Sync`: Ruby hands the same instance to every thread, and a
|
|
43
|
+
// `RefCell` shared that way is unsound (see `BPETokenizer`'s lock).
|
|
44
|
+
state: Mutex<EncodeState>,
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
impl SentencePieceTokenizer {
|
|
@@ -48,8 +51,8 @@ impl SentencePieceTokenizer {
|
|
|
48
51
|
let tokenizer = crate::cache::apply_max_cache_bytes_sp(tokenizer);
|
|
49
52
|
let state = EncodeState::with_budget(tokenizer.max_cache_bytes());
|
|
50
53
|
Self {
|
|
51
|
-
tokenizer
|
|
52
|
-
state:
|
|
54
|
+
tokenizer,
|
|
55
|
+
state: Mutex::new(state),
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -58,8 +61,8 @@ impl SentencePieceTokenizer {
|
|
|
58
61
|
let bytes = unsafe { input.as_slice() };
|
|
59
62
|
let text = require_utf8(ruby, bytes)?;
|
|
60
63
|
let mut ids: Vec<u32> = Vec::new();
|
|
61
|
-
let mut state = rb_self.state.
|
|
62
|
-
rb_self.tokenizer.
|
|
64
|
+
let mut state = rb_self.state.lock().unwrap_or_else(|e| e.into_inner());
|
|
65
|
+
rb_self.tokenizer.encode_raw_cb(&mut state, text, &mut |tokens| {
|
|
63
66
|
ids.extend(tokens.iter().map(|&t| u32::from(t)))
|
|
64
67
|
});
|
|
65
68
|
Ok(ids)
|
|
@@ -84,8 +87,7 @@ impl SentencePieceTokenizer {
|
|
|
84
87
|
})
|
|
85
88
|
.collect::<Result<_, _>>()?;
|
|
86
89
|
let doc_refs: Vec<&str> = docs.iter().map(String::as_str).collect();
|
|
87
|
-
let tokenizer = rb_self.tokenizer
|
|
88
|
-
let tokenizer: &SentencePieceBPE = &tokenizer;
|
|
90
|
+
let tokenizer: &SentencePieceBPE = &rb_self.tokenizer;
|
|
89
91
|
Ok(without_gvl(|| sp_encode_docs_ragged(tokenizer, &doc_refs)))
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -130,8 +132,7 @@ impl SentencePieceTokenizer {
|
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
let tokenizer = rb_self.tokenizer
|
|
134
|
-
let tokenizer: &SentencePieceBPE = &tokenizer;
|
|
135
|
+
let tokenizer: &SentencePieceBPE = &rb_self.tokenizer;
|
|
135
136
|
let encoded: std::io::Result<(Vec<u32>, Vec<i64>)> = without_gvl(|| {
|
|
136
137
|
sources::encode_files_ragged(&source, parallel, |files, format| {
|
|
137
138
|
for ®ion in files {
|
|
@@ -166,16 +167,16 @@ impl SentencePieceTokenizer {
|
|
|
166
167
|
fn decode(ruby: &Ruby, rb_self: &Self, tokens: RArray) -> Result<RString, Error> {
|
|
167
168
|
let ids: Vec<u32> = tokens.to_vec()?;
|
|
168
169
|
let ids: Vec<_> = ids.into_iter().map(Into::into).collect();
|
|
169
|
-
let bytes = rb_self.tokenizer.
|
|
170
|
+
let bytes = rb_self.tokenizer.decode(&ids);
|
|
170
171
|
Ok(binary_string(ruby, &bytes))
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
fn vocab_size(&self) -> usize {
|
|
174
|
-
self.tokenizer.
|
|
175
|
+
self.tokenizer.vocab_size()
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
fn vocab(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
|
|
178
|
-
let tokenizer = rb_self.tokenizer
|
|
179
|
+
let tokenizer = &rb_self.tokenizer;
|
|
179
180
|
let hash = ruby.hash_new();
|
|
180
181
|
for (id, bytes) in tokenizer.vocab_entries() {
|
|
181
182
|
hash.aset(id, binary_string(ruby, bytes))?;
|
|
@@ -184,7 +185,7 @@ impl SentencePieceTokenizer {
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
fn merges(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
187
|
-
let tokenizer = rb_self.tokenizer
|
|
188
|
+
let tokenizer = &rb_self.tokenizer;
|
|
188
189
|
let entries = tokenizer.merge_entries();
|
|
189
190
|
let result = ruby.ary_new_capa(entries.len());
|
|
190
191
|
for (a, b) in entries {
|
|
@@ -196,7 +197,7 @@ impl SentencePieceTokenizer {
|
|
|
196
197
|
/// Cached unit entries on the single-document `encode` path's state
|
|
197
198
|
/// (batch encoders are per-call); see `BPETokenizer::cache_entries`.
|
|
198
199
|
fn cache_entries(&self) -> usize {
|
|
199
|
-
self.state.
|
|
200
|
+
self.state.lock().unwrap_or_else(|e| e.into_inner()).cache_size()
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//! the core crate's `src/lib.rs` (the `python` feature), minus the
|
|
4
4
|
//! numpy/awkward-array machinery that has no Ruby analog.
|
|
5
5
|
|
|
6
|
-
use std::
|
|
6
|
+
use std::sync::RwLock;
|
|
7
7
|
use std::collections::{HashMap, HashSet};
|
|
8
8
|
use std::os::raw::c_long;
|
|
9
9
|
|
|
@@ -353,14 +353,14 @@ fn marshal_inputs(inputs: RArray) -> Result<InputDocs, Error> {
|
|
|
353
353
|
|
|
354
354
|
#[magnus::wrap(class = "Gigatoken::Native::BPETokenizer", free_immediately, size)]
|
|
355
355
|
pub struct BPETokenizer {
|
|
356
|
-
tokenizer:
|
|
356
|
+
tokenizer: RwLock<Tokenizer>,
|
|
357
357
|
workers: WorkerPool,
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
impl BPETokenizer {
|
|
361
361
|
pub(crate) fn from_tokenizer(tokenizer: Tokenizer) -> Self {
|
|
362
362
|
Self {
|
|
363
|
-
tokenizer:
|
|
363
|
+
tokenizer: RwLock::new(crate::cache::apply_max_cache_bytes(tokenizer)),
|
|
364
364
|
workers: WorkerPool::new(),
|
|
365
365
|
}
|
|
366
366
|
}
|
|
@@ -407,14 +407,81 @@ impl BPETokenizer {
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/// Shared access to the tokenizer, for everything that only reads it —
|
|
411
|
+
/// the batch paths, `decode`, `vocab`, `merges`, the size accessors.
|
|
412
|
+
///
|
|
413
|
+
/// Readers never exclude each other, which is what makes the long holds
|
|
414
|
+
/// safe: `encode_batch`/`encode_files` keep this across a GVL release
|
|
415
|
+
/// (they run the core pool over `&Tokenizer`), and any other Ruby thread
|
|
416
|
+
/// reading meanwhile just proceeds. The only exclusive holder is
|
|
417
|
+
/// [`Self::encode`], which is short.
|
|
418
|
+
fn read_tokenizer(&self) -> std::sync::RwLockReadGuard<'_, Tokenizer> {
|
|
419
|
+
self.tokenizer.read().unwrap_or_else(|e| e.into_inner())
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Encode one string, mutating the tokenizer's pretoken cache — the only
|
|
423
|
+
/// exclusive use of the lock.
|
|
424
|
+
///
|
|
425
|
+
/// Uncontended (every single-threaded caller, and the common case under
|
|
426
|
+
/// threads) this takes the fast path: grab the write guard, encode against
|
|
427
|
+
/// the Ruby string's own bytes, never release the GVL. Identical cost to
|
|
428
|
+
/// the pre-lock version plus one uncontended atomic.
|
|
429
|
+
///
|
|
430
|
+
/// Contended, the writer is waiting on a reader that will hold the lock
|
|
431
|
+
/// for as long as a batch encode takes. Blocking there while holding the
|
|
432
|
+
/// GVL would stall every other Ruby thread in the VM, so instead the input
|
|
433
|
+
/// is copied and the whole wait-and-encode moves inside `without_gvl`.
|
|
434
|
+
/// The copy is what makes that sound: no Ruby `VALUE` and no `RString`
|
|
435
|
+
/// buffer may outlive the release (see `marshal_inputs`), and the guard is
|
|
436
|
+
/// taken and dropped inside the closure, so it never crosses OS threads
|
|
437
|
+
/// even when the scheduler offloads it (see `gvl`).
|
|
410
438
|
fn encode(&self, input: RString) -> Vec<u32> {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
.encode_with_added_tokens_flat(bytes, &mut out);
|
|
417
|
-
|
|
439
|
+
if let Ok(mut tokenizer) = self.tokenizer.try_write() {
|
|
440
|
+
// SAFETY: read-only, for the duration of this synchronous call,
|
|
441
|
+
// with no GVL release in between.
|
|
442
|
+
let bytes = unsafe { input.as_slice() };
|
|
443
|
+
let mut out = Vec::new();
|
|
444
|
+
tokenizer.encode_with_added_tokens_flat(bytes, &mut out);
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
self.encode_contended(input)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/// The contended half of [`Self::encode`], outlined and `#[cold]`.
|
|
452
|
+
///
|
|
453
|
+
/// Keeping this out of `encode`'s body is a measured requirement, not
|
|
454
|
+
/// tidiness: the workspace builds with `lto = "fat"`, so the core encode
|
|
455
|
+
/// routine inlines into `encode`, and inlining is sensitive to the caller's
|
|
456
|
+
/// size. Written inline, this second path measured slower on single
|
|
457
|
+
/// encodes — no lock overhead, just a flipped inlining decision. Outlined,
|
|
458
|
+
/// `encode`'s hot body is the original three lines behind a `try_write`.
|
|
459
|
+
///
|
|
460
|
+
/// Before you re-inline this "to simplify": rerun the evidence rather than
|
|
461
|
+
/// trusting a number. `ruby -Ilib bench/encode_ab.rb` with the attributes
|
|
462
|
+
/// stripped and again with them restored, and read
|
|
463
|
+
/// `docs/rb/benchmarks.md` first — no size resolves this on the hardware
|
|
464
|
+
/// measured so far. The instrument is honest (an interleaved same-build
|
|
465
|
+
/// run never calls a size faster or slower, at any size) and has power to
|
|
466
|
+
/// catch a couple-percent effect reliably, but the attributes' real
|
|
467
|
+
/// effect is small enough that even the tightest floor (medium,
|
|
468
|
+
/// well under 3%) swallows it more often than not. Removing the
|
|
469
|
+
/// attributes measures slower at medium and large, in the direction the
|
|
470
|
+
/// outlining was added to prevent, but neither delta clears the noise
|
|
471
|
+
/// floor. Keep it outlined on that direction and on the original
|
|
472
|
+
/// inline-regression measurement, not on a pinned-down magnitude.
|
|
473
|
+
#[cold]
|
|
474
|
+
#[inline(never)]
|
|
475
|
+
fn encode_contended(&self, input: RString) -> Vec<u32> {
|
|
476
|
+
// SAFETY: copied before any GVL release, so nothing Ruby-owned is
|
|
477
|
+
// captured by the closure below.
|
|
478
|
+
let owned = unsafe { input.as_slice() }.to_vec();
|
|
479
|
+
without_gvl(move || {
|
|
480
|
+
let mut tokenizer = self.tokenizer.write().unwrap_or_else(|e| e.into_inner());
|
|
481
|
+
let mut out = Vec::new();
|
|
482
|
+
tokenizer.encode_with_added_tokens_flat(&owned, &mut out);
|
|
483
|
+
out
|
|
484
|
+
})
|
|
418
485
|
}
|
|
419
486
|
|
|
420
487
|
/// Encode a batch on the core worker pool, with the GVL released for the
|
|
@@ -425,7 +492,7 @@ impl BPETokenizer {
|
|
|
425
492
|
fn encode_batch_ragged(rb_self: &Self, inputs: RArray) -> Result<(Vec<u32>, Vec<i64>), Error> {
|
|
426
493
|
let marshaled = marshal_inputs(inputs)?;
|
|
427
494
|
let doc_slices = marshaled.as_slices();
|
|
428
|
-
let tokenizer = rb_self.
|
|
495
|
+
let tokenizer = rb_self.read_tokenizer();
|
|
429
496
|
let tokenizer: &Tokenizer = &tokenizer;
|
|
430
497
|
let workers = &rb_self.workers;
|
|
431
498
|
Ok(without_gvl(|| encode_docs_ragged(workers, tokenizer, &doc_slices)))
|
|
@@ -474,7 +541,7 @@ impl BPETokenizer {
|
|
|
474
541
|
// the duration of the gather below (see the allocation above).
|
|
475
542
|
let dest = unsafe { GatherBuf::new(ptr, total_bytes) };
|
|
476
543
|
|
|
477
|
-
let tokenizer = rb_self.
|
|
544
|
+
let tokenizer = rb_self.read_tokenizer();
|
|
478
545
|
let tokenizer: &Tokenizer = &tokenizer;
|
|
479
546
|
let workers = &rb_self.workers;
|
|
480
547
|
match without_gvl(|| encode_docs_into(workers, tokenizer, &doc_slices, dest)) {
|
|
@@ -515,7 +582,7 @@ impl BPETokenizer {
|
|
|
515
582
|
};
|
|
516
583
|
|
|
517
584
|
let source = sources::resolve(ruby, source)?;
|
|
518
|
-
let tokenizer = rb_self.
|
|
585
|
+
let tokenizer = rb_self.read_tokenizer();
|
|
519
586
|
let tokenizer: &Tokenizer = &tokenizer;
|
|
520
587
|
let workers = &rb_self.workers;
|
|
521
588
|
let encoded: std::io::Result<(Vec<u32>, Vec<i64>)> = without_gvl(|| {
|
|
@@ -547,16 +614,16 @@ impl BPETokenizer {
|
|
|
547
614
|
fn decode(ruby: &Ruby, rb_self: &Self, tokens: RArray) -> Result<RString, Error> {
|
|
548
615
|
let ids: Vec<u32> = tokens.to_vec()?;
|
|
549
616
|
let ids: Vec<_> = ids.into_iter().map(Into::into).collect();
|
|
550
|
-
let bytes: Vec<u8> = rb_self.
|
|
617
|
+
let bytes: Vec<u8> = rb_self.read_tokenizer().decode(&ids).collect();
|
|
551
618
|
Ok(binary_string(ruby, &bytes))
|
|
552
619
|
}
|
|
553
620
|
|
|
554
621
|
fn vocab_size(&self) -> usize {
|
|
555
|
-
self.
|
|
622
|
+
self.read_tokenizer().vocab_size()
|
|
556
623
|
}
|
|
557
624
|
|
|
558
625
|
fn vocab(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
|
|
559
|
-
let tokenizer = rb_self.
|
|
626
|
+
let tokenizer = rb_self.read_tokenizer();
|
|
560
627
|
let hash = ruby.hash_new();
|
|
561
628
|
for (id, bytes) in tokenizer.vocab_entries() {
|
|
562
629
|
hash.aset(id, binary_string(ruby, bytes))?;
|
|
@@ -565,7 +632,7 @@ impl BPETokenizer {
|
|
|
565
632
|
}
|
|
566
633
|
|
|
567
634
|
fn merges(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
568
|
-
let tokenizer = rb_self.
|
|
635
|
+
let tokenizer = rb_self.read_tokenizer();
|
|
569
636
|
let entries = tokenizer.merge_entries();
|
|
570
637
|
let result = ruby.ary_new_capa(entries.len());
|
|
571
638
|
for (a, b) in entries {
|
|
@@ -578,7 +645,7 @@ impl BPETokenizer {
|
|
|
578
645
|
/// drops back toward vocab-seed level when a budgeted cache wipes (see
|
|
579
646
|
/// `Gigatoken.max_cache_bytes`).
|
|
580
647
|
fn cache_entries(&self) -> usize {
|
|
581
|
-
self.
|
|
648
|
+
self.read_tokenizer().cache_entries()
|
|
582
649
|
}
|
|
583
650
|
}
|
|
584
651
|
|
data/lib/gigatoken/version.rb
CHANGED