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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 071b3abb52387367f392e88e50d92c7413f5f6c70dae24accf6d378ad6e78fd8
4
- data.tar.gz: 26da76745e877b6ee30821ab5fcae50aefdb6ecddcd660e9b8c7b48b4d94bd06
3
+ metadata.gz: eee37e720c1b0fba0305398b731c3bd909af1a6ebc1ae7393e924c66fd629377
4
+ data.tar.gz: 966c52f87dcff1b0cda6218c45b0cbc5362a02655cb0c570034b4d91249a2518
5
5
  SHA512:
6
- metadata.gz: 402d1e74262ffd0d4ff8d6f6162c944747b75c567249f788fdf46d2c5e09cd001f1cd66d037156848023fb5aad62087c73def5c4d3134d2aba7a898575ce2631
7
- data.tar.gz: c43c48249a2c7bd0d8146e20d830975dae6e3acec93b1bc42e125fdd72050da38d02d0a10068b6ad208c85bd1790a54629b3b2394c1b12762feddce834909995
6
+ metadata.gz: 75eda453e48567dda4b39765031ed1dab4e5d12715507f0515e6742de2dd0ce67a9c4b5e4c1581664bce956b01cf8978cc9198ee3878253bd26287ba0d558aa2
7
+ data.tar.gz: 3a4e18c3e8603ae524cfa48752177277297c94343623571ad32bb87434eacfa3456264769f9b3daf5421ad5d615de47502607db17e6ded024e8c9b35a542b9c1
data/Cargo.lock CHANGED
@@ -771,7 +771,7 @@ dependencies = [
771
771
 
772
772
  [[package]]
773
773
  name = "gigatoken-rb"
774
- version = "0.2.0"
774
+ version = "0.2.1"
775
775
  dependencies = [
776
776
  "gigatoken",
777
777
  "magnus",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "gigatoken-rb"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -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::cell::RefCell;
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 this `RefCell` is never `borrow_mut`'dsee the
39
- // builder report's DISAGREEMENTS for why it's here anyway.
40
- tokenizer: RefCell<SentencePieceBPE>,
41
- state: RefCell<EncodeState>,
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: RefCell::new(tokenizer),
52
- state: RefCell::new(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.borrow_mut();
62
- rb_self.tokenizer.borrow().encode_raw_cb(&mut state, text, &mut |tokens| {
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.borrow();
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.borrow();
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 &region 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.borrow().decode(&ids);
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.borrow().vocab_size()
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.borrow();
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.borrow();
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.borrow().cache_size()
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::cell::RefCell;
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: RefCell<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: RefCell::new(crate::cache::apply_max_cache_bytes(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
- // SAFETY: read-only, for the duration of this synchronous call.
412
- let bytes = unsafe { input.as_slice() };
413
- let mut out = Vec::new();
414
- self.tokenizer
415
- .borrow_mut()
416
- .encode_with_added_tokens_flat(bytes, &mut out);
417
- out
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.tokenizer.borrow();
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.tokenizer.borrow();
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.tokenizer.borrow();
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.tokenizer.borrow().decode(&ids).collect();
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.tokenizer.borrow().vocab_size()
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.tokenizer.borrow();
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.tokenizer.borrow();
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.tokenizer.borrow().cache_entries()
648
+ self.read_tokenizer().cache_entries()
582
649
  }
583
650
  }
584
651
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gigatoken
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gigatoken
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs