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/src/batch.rs
CHANGED
|
@@ -12,7 +12,7 @@ use crate::input::DocumentIter;
|
|
|
12
12
|
use crate::input::file_source::{DocFormat, chunk_ranges};
|
|
13
13
|
use std::ops::Range;
|
|
14
14
|
use std::cell::UnsafeCell;
|
|
15
|
-
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
15
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
16
16
|
use std::sync::{Mutex, OnceLock, TryLockError};
|
|
17
17
|
|
|
18
18
|
/// Parallel chunks must hold at least this many bytes: a chunk this size
|
|
@@ -123,7 +123,17 @@ pub(crate) struct ChunkTokens {
|
|
|
123
123
|
pub(crate) continues: bool,
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
/// Encode one chunk. `cancel` is polled per document rather than only
|
|
127
|
+
/// between chunks: a chunk is a megabyte or more of input, and the caller's
|
|
128
|
+
/// interrupt should not wait out the one already in flight. What is left is
|
|
129
|
+
/// a partial `ChunkTokens`, which is safe because a cancelled run discards
|
|
130
|
+
/// every chunk it encoded — including a run of one chunk (see
|
|
131
|
+
/// `encode_chunks_gathered`).
|
|
132
|
+
fn encode_chunk(
|
|
133
|
+
tokenizer: &mut Tokenizer,
|
|
134
|
+
chunk: &EncodeChunk,
|
|
135
|
+
cancel: Option<&AtomicBool>,
|
|
136
|
+
) -> ChunkTokens {
|
|
127
137
|
// Reserve the output once, from a bytes-per-token estimate on the low
|
|
128
138
|
// side of natural language (~4.4 on OWT/GPT-2). Growing from empty
|
|
129
139
|
// instead re-copies roughly the final size in doublings — per chunk,
|
|
@@ -141,12 +151,20 @@ fn encode_chunk(tokenizer: &mut Tokenizer, chunk: &EncodeChunk) -> ChunkTokens {
|
|
|
141
151
|
match chunk {
|
|
142
152
|
EncodeChunk::Docs(docs) => {
|
|
143
153
|
for doc in docs {
|
|
154
|
+
if cancelled(cancel) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
144
157
|
encode_into(tokenizer, doc, &mut ids, &mut lens);
|
|
145
158
|
}
|
|
146
159
|
}
|
|
147
160
|
EncodeChunk::Region { bytes, format } => {
|
|
148
161
|
for_each_doc(bytes, format, |doc| {
|
|
149
|
-
|
|
162
|
+
// `for_each_doc` has no break; once cancelled the rest of
|
|
163
|
+
// the region is walked but not encoded (see the sequential
|
|
164
|
+
// path in `encode_files_docs_serial_with`).
|
|
165
|
+
if !cancelled(cancel) {
|
|
166
|
+
encode_into(tokenizer, doc, &mut ids, &mut lens)
|
|
167
|
+
}
|
|
150
168
|
})
|
|
151
169
|
}
|
|
152
170
|
EncodeChunk::Fragment { bytes, first } => {
|
|
@@ -621,9 +639,29 @@ impl Committer {
|
|
|
621
639
|
}
|
|
622
640
|
}
|
|
623
641
|
|
|
642
|
+
/// Whether the caller's cancellation token has been set — polled between
|
|
643
|
+
/// chunks by the loops below and per document inside one (`encode_chunk`),
|
|
644
|
+
/// so an interrupt waits out a document rather than a whole chunk.
|
|
645
|
+
/// `None` is a run that cannot be cancelled: what the free functions this
|
|
646
|
+
/// module exports (the pyo3 bindings' and the benches' entry points) pass,
|
|
647
|
+
/// and why they may `expect` a result.
|
|
648
|
+
///
|
|
649
|
+
/// The flag carries no data, only the decision to stop, so `Relaxed` is all
|
|
650
|
+
/// the ordering it needs. What sets it is the Ruby extension's unblock
|
|
651
|
+
/// function, from whichever thread Ruby delivers the interrupt on — see
|
|
652
|
+
/// `ext/gigatoken/src/gvl.rs`.
|
|
653
|
+
fn cancelled(cancel: Option<&AtomicBool>) -> bool {
|
|
654
|
+
cancel.is_some_and(|flag| flag.load(Ordering::Relaxed))
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/// What an uncancellable entry point `expect`s of the cancellable core.
|
|
658
|
+
const UNCANCELLABLE: &str = "a run with no cancellation token encodes every chunk";
|
|
659
|
+
|
|
624
660
|
/// Encode all chunks with pooled workers and gather them into one flat id
|
|
625
661
|
/// buffer plus per-document row counts — in parallel when there is more
|
|
626
|
-
/// than one chunk, serially otherwise.
|
|
662
|
+
/// than one chunk, serially otherwise. `None` means `cancel` was set part
|
|
663
|
+
/// way through: the chunks encoded so far are dropped, since a batch missing
|
|
664
|
+
/// its tail is nothing a caller can use. Each worker's caches are pre-sized
|
|
627
665
|
/// for its share of `total_bytes` (capacity hints only — see
|
|
628
666
|
/// `Tokenizer::fork_sized`; workers already forked on an earlier call keep
|
|
629
667
|
/// their warm caches).
|
|
@@ -642,10 +680,11 @@ pub(crate) fn encode_chunks_gathered(
|
|
|
642
680
|
proto: &Tokenizer,
|
|
643
681
|
chunks: &[EncodeChunk],
|
|
644
682
|
total_bytes: usize,
|
|
645
|
-
|
|
683
|
+
cancel: Option<&AtomicBool>,
|
|
684
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
646
685
|
// A token consumes >= 1 input byte, so total_bytes tokens is the
|
|
647
686
|
// reservation bound (NFC expansion is caught by the overflow escape).
|
|
648
|
-
encode_chunks_gathered_with_cap(workers, proto, chunks, total_bytes, total_bytes)
|
|
687
|
+
encode_chunks_gathered_with_cap(workers, proto, chunks, total_bytes, total_bytes, cancel)
|
|
649
688
|
}
|
|
650
689
|
|
|
651
690
|
/// `encode_chunks_gathered` with the committer's reservation bound passed
|
|
@@ -656,20 +695,23 @@ fn encode_chunks_gathered_with_cap(
|
|
|
656
695
|
chunks: &[EncodeChunk],
|
|
657
696
|
total_bytes: usize,
|
|
658
697
|
cap_tokens: usize,
|
|
659
|
-
|
|
698
|
+
cancel: Option<&AtomicBool>,
|
|
699
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
660
700
|
let share = total_bytes / rayon::current_num_threads().max(1);
|
|
661
|
-
let encode =
|
|
701
|
+
let encode =
|
|
702
|
+
|c: &EncodeChunk| workers.with_worker(proto, share, |tok| encode_chunk(tok, c, cancel));
|
|
662
703
|
if chunks.len() <= 1 {
|
|
663
704
|
// Small inputs skip the thread fan-out — and a lone chunk's id
|
|
664
|
-
// buffer IS the flat result, no gather copy at all.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
None => (Vec::new(), Vec::new()),
|
|
705
|
+
// buffer IS the flat result, no gather copy at all. `encode_chunk`
|
|
706
|
+
// polls the token per document, so even a lone chunk can come back
|
|
707
|
+
// short; report that as a cancelled run like every other path
|
|
708
|
+
// rather than returning the truncated prefix.
|
|
709
|
+
let Some(chunk) = chunks.first() else {
|
|
710
|
+
return Some((Vec::new(), Vec::new()));
|
|
672
711
|
};
|
|
712
|
+
let out = encode(chunk);
|
|
713
|
+
let counts = row_counts(std::slice::from_ref(&out));
|
|
714
|
+
return (!cancelled(cancel)).then(|| (out.ids, counts));
|
|
673
715
|
}
|
|
674
716
|
let next = AtomicUsize::new(0);
|
|
675
717
|
let outs: Vec<OnceLock<ChunkTokens>> = (0..chunks.len()).map(|_| OnceLock::new()).collect();
|
|
@@ -678,7 +720,10 @@ fn encode_chunks_gathered_with_cap(
|
|
|
678
720
|
rayon::scope(|s| {
|
|
679
721
|
for _ in 0..tasks {
|
|
680
722
|
s.spawn(|_| {
|
|
681
|
-
|
|
723
|
+
// A cancelled run is discarded whole, so a worker that sees
|
|
724
|
+
// the token set just stops claiming chunks — no drain, no
|
|
725
|
+
// commit, nothing to salvage.
|
|
726
|
+
while !cancelled(cancel) {
|
|
682
727
|
let i = next.fetch_add(1, Ordering::Relaxed);
|
|
683
728
|
let Some(chunk) = chunks.get(i) else {
|
|
684
729
|
// One last opportunistic drain on the way out: this
|
|
@@ -699,20 +744,23 @@ fn encode_chunks_gathered_with_cap(
|
|
|
699
744
|
});
|
|
700
745
|
}
|
|
701
746
|
});
|
|
747
|
+
if cancelled(cancel) {
|
|
748
|
+
return None;
|
|
749
|
+
}
|
|
702
750
|
let outs: Vec<ChunkTokens> = outs
|
|
703
751
|
.into_iter()
|
|
704
752
|
.map(|slot| slot.into_inner().expect("every claimed chunk was encoded"))
|
|
705
753
|
.collect();
|
|
706
754
|
let counts = row_counts(&outs);
|
|
707
755
|
let total: usize = outs.iter().map(|c| c.ids.len()).sum();
|
|
708
|
-
match committer.and_then(|c| c.finish(&outs, total)) {
|
|
756
|
+
Some(match committer.and_then(|c| c.finish(&outs, total)) {
|
|
709
757
|
Some(flat) => {
|
|
710
758
|
// The copies are done; the spent chunk buffers are dead weight.
|
|
711
759
|
defer_drop(outs);
|
|
712
760
|
(flat, counts)
|
|
713
761
|
}
|
|
714
762
|
None => (gather_flat(outs), counts),
|
|
715
|
-
}
|
|
763
|
+
})
|
|
716
764
|
}
|
|
717
765
|
|
|
718
766
|
/// `encode_chunks_gathered_with_cap`, but gathering directly into a
|
|
@@ -721,34 +769,41 @@ fn encode_chunks_gathered_with_cap(
|
|
|
721
769
|
/// `encode_docs_into` and `ext/gigatoken/src/tokenizer.rs`). Every chunk is
|
|
722
770
|
/// encoded either way, so an overrun `GatherOutcome::Fallback` carries the
|
|
723
771
|
/// classic gathered result rather than asking the caller to re-run the
|
|
724
|
-
/// encode.
|
|
772
|
+
/// encode. `None` means `cancel` was set part way through (see
|
|
773
|
+
/// `encode_chunks_gathered`), leaving `dest` partly written and unusable.
|
|
725
774
|
pub(crate) fn encode_chunks_into(
|
|
726
775
|
workers: &WorkerPool,
|
|
727
776
|
proto: &Tokenizer,
|
|
728
777
|
chunks: &[EncodeChunk],
|
|
729
778
|
total_bytes: usize,
|
|
730
779
|
dest: GatherBuf,
|
|
731
|
-
|
|
780
|
+
cancel: Option<&AtomicBool>,
|
|
781
|
+
) -> Option<GatherOutcome> {
|
|
732
782
|
let share = total_bytes / rayon::current_num_threads().max(1);
|
|
733
|
-
let encode =
|
|
783
|
+
let encode =
|
|
784
|
+
|c: &EncodeChunk| workers.with_worker(proto, share, |tok| encode_chunk(tok, c, cancel));
|
|
734
785
|
if chunks.len() <= 1 {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
// SAFETY: the only chunk, so the only write; in bounds
|
|
741
|
-
// per the check above.
|
|
742
|
-
unsafe {
|
|
743
|
-
std::ptr::copy_nonoverlapping(out.ids.as_ptr(), dest.ptr, out.ids.len());
|
|
744
|
-
}
|
|
745
|
-
GatherOutcome::Committed(out.ids.len(), counts)
|
|
746
|
-
} else {
|
|
747
|
-
GatherOutcome::Fallback(out.ids, counts)
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
None => GatherOutcome::Committed(0, Vec::new()),
|
|
786
|
+
// A lone chunk can still be cancelled part way through (see
|
|
787
|
+
// `encode_chunks_gathered_with_cap`); `dest` is then left partly
|
|
788
|
+
// written, exactly as on the parallel path.
|
|
789
|
+
let Some(chunk) = chunks.first() else {
|
|
790
|
+
return Some(GatherOutcome::Committed(0, Vec::new()));
|
|
751
791
|
};
|
|
792
|
+
let out = encode(chunk);
|
|
793
|
+
let counts = row_counts(std::slice::from_ref(&out));
|
|
794
|
+
if cancelled(cancel) {
|
|
795
|
+
return None;
|
|
796
|
+
}
|
|
797
|
+
return Some(if out.ids.len() <= dest.cap {
|
|
798
|
+
// SAFETY: the only chunk, so the only write; in bounds per the
|
|
799
|
+
// check above.
|
|
800
|
+
unsafe {
|
|
801
|
+
std::ptr::copy_nonoverlapping(out.ids.as_ptr(), dest.ptr, out.ids.len());
|
|
802
|
+
}
|
|
803
|
+
GatherOutcome::Committed(out.ids.len(), counts)
|
|
804
|
+
} else {
|
|
805
|
+
GatherOutcome::Fallback(out.ids, counts)
|
|
806
|
+
});
|
|
752
807
|
}
|
|
753
808
|
let next = AtomicUsize::new(0);
|
|
754
809
|
let outs: Vec<OnceLock<ChunkTokens>> = (0..chunks.len()).map(|_| OnceLock::new()).collect();
|
|
@@ -757,7 +812,10 @@ pub(crate) fn encode_chunks_into(
|
|
|
757
812
|
rayon::scope(|s| {
|
|
758
813
|
for _ in 0..tasks {
|
|
759
814
|
s.spawn(|_| {
|
|
760
|
-
|
|
815
|
+
// Stop claiming chunks once cancelled (see
|
|
816
|
+
// `encode_chunks_gathered_with_cap`); the destination is
|
|
817
|
+
// left half-written for the caller to discard or reuse.
|
|
818
|
+
while !cancelled(cancel) {
|
|
761
819
|
let i = next.fetch_add(1, Ordering::Relaxed);
|
|
762
820
|
let Some(chunk) = chunks.get(i) else {
|
|
763
821
|
// One last opportunistic drain on the way out: this
|
|
@@ -774,19 +832,22 @@ pub(crate) fn encode_chunks_into(
|
|
|
774
832
|
});
|
|
775
833
|
}
|
|
776
834
|
});
|
|
835
|
+
if cancelled(cancel) {
|
|
836
|
+
return None;
|
|
837
|
+
}
|
|
777
838
|
let outs: Vec<ChunkTokens> = outs
|
|
778
839
|
.into_iter()
|
|
779
840
|
.map(|slot| slot.into_inner().expect("every claimed chunk was encoded"))
|
|
780
841
|
.collect();
|
|
781
842
|
let counts = row_counts(&outs);
|
|
782
843
|
let total: usize = outs.iter().map(|c| c.ids.len()).sum();
|
|
783
|
-
if committer.finish_external(&outs, total) {
|
|
844
|
+
Some(if committer.finish_external(&outs, total) {
|
|
784
845
|
// The copies are done; the spent chunk buffers are dead weight.
|
|
785
846
|
defer_drop(outs);
|
|
786
847
|
GatherOutcome::Committed(total, counts)
|
|
787
848
|
} else {
|
|
788
849
|
GatherOutcome::Fallback(gather_flat(outs), counts)
|
|
789
|
-
}
|
|
850
|
+
})
|
|
790
851
|
}
|
|
791
852
|
|
|
792
853
|
/// Merge per-chunk outputs into one flat id buffer and per-document row
|
|
@@ -922,6 +983,59 @@ impl WorkerPool {
|
|
|
922
983
|
});
|
|
923
984
|
f(guard.get_or_insert_with(|| proto.fork_sized(expected_bytes)))
|
|
924
985
|
}
|
|
986
|
+
|
|
987
|
+
/// `encode_docs_ragged` with a cancellation token: the same encode, plus
|
|
988
|
+
/// a flag polled at chunk granularity, and `None` when it was set part
|
|
989
|
+
/// way through (see `encode_chunks_gathered`).
|
|
990
|
+
///
|
|
991
|
+
/// This and the three below are methods rather than free functions like
|
|
992
|
+
/// their uncancellable twins: the crate's free-function set is the
|
|
993
|
+
/// surface the pyo3 bindings and the Rust benches are written against,
|
|
994
|
+
/// and the pool is the receiver either way. They are what the Ruby
|
|
995
|
+
/// extension calls, so that an interrupt cancels a batch instead of
|
|
996
|
+
/// waiting it out (`ext/gigatoken/src/gvl.rs`).
|
|
997
|
+
pub fn encode_docs_ragged_cancellable(
|
|
998
|
+
&self,
|
|
999
|
+
proto: &Tokenizer,
|
|
1000
|
+
docs: &[&[u8]],
|
|
1001
|
+
cancel: &AtomicBool,
|
|
1002
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
1003
|
+
encode_docs_ragged_with(self, proto, docs, lpt_from_env(), Some(cancel))
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/// `encode_docs_into` with a cancellation token.
|
|
1007
|
+
pub fn encode_docs_into_cancellable(
|
|
1008
|
+
&self,
|
|
1009
|
+
proto: &Tokenizer,
|
|
1010
|
+
docs: &[&[u8]],
|
|
1011
|
+
dest: GatherBuf,
|
|
1012
|
+
cancel: &AtomicBool,
|
|
1013
|
+
) -> Option<GatherOutcome> {
|
|
1014
|
+
encode_docs_into_with(self, proto, docs, dest, Some(cancel))
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/// `encode_files_docs` with a cancellation token.
|
|
1018
|
+
pub fn encode_files_docs_cancellable(
|
|
1019
|
+
&self,
|
|
1020
|
+
proto: &Tokenizer,
|
|
1021
|
+
files: &[&[u8]],
|
|
1022
|
+
format: &DocFormat,
|
|
1023
|
+
cancel: &AtomicBool,
|
|
1024
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
1025
|
+
encode_files_docs_with(self, proto, files, format, Some(cancel))
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/// `encode_files_docs_serial` with a cancellation token, polled per
|
|
1029
|
+
/// document.
|
|
1030
|
+
pub fn encode_files_docs_serial_cancellable(
|
|
1031
|
+
&self,
|
|
1032
|
+
proto: &Tokenizer,
|
|
1033
|
+
files: &[&[u8]],
|
|
1034
|
+
format: &DocFormat,
|
|
1035
|
+
cancel: &AtomicBool,
|
|
1036
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
1037
|
+
encode_files_docs_serial_with(self, proto, files, format, Some(cancel))
|
|
1038
|
+
}
|
|
925
1039
|
}
|
|
926
1040
|
|
|
927
1041
|
/// Shared core of encode_batch / encode_files for pre-resolved document
|
|
@@ -939,7 +1053,7 @@ pub fn encode_docs_ragged(
|
|
|
939
1053
|
proto: &Tokenizer,
|
|
940
1054
|
docs: &[&[u8]],
|
|
941
1055
|
) -> (Vec<u32>, Vec<i64>) {
|
|
942
|
-
encode_docs_ragged_with(workers, proto, docs, lpt_from_env())
|
|
1056
|
+
encode_docs_ragged_with(workers, proto, docs, lpt_from_env(), None).expect(UNCANCELLABLE)
|
|
943
1057
|
}
|
|
944
1058
|
|
|
945
1059
|
/// `encode_docs_ragged` with the LPT switch passed explicitly instead of
|
|
@@ -950,11 +1064,12 @@ pub(crate) fn encode_docs_ragged_with(
|
|
|
950
1064
|
proto: &Tokenizer,
|
|
951
1065
|
docs: &[&[u8]],
|
|
952
1066
|
lpt: bool,
|
|
953
|
-
|
|
1067
|
+
cancel: Option<&AtomicBool>,
|
|
1068
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
954
1069
|
let total: usize = docs.iter().map(|d| d.len()).sum();
|
|
955
1070
|
let added = proto.added_token_split_blockers();
|
|
956
1071
|
let chunks = build_doc_chunks(docs, total, chunk_target_bytes(total), &added, lpt);
|
|
957
|
-
encode_chunks_gathered(workers, proto, &chunks, total)
|
|
1072
|
+
encode_chunks_gathered(workers, proto, &chunks, total, cancel)
|
|
958
1073
|
}
|
|
959
1074
|
|
|
960
1075
|
/// `encode_docs_ragged`, but gathering directly into a caller-supplied
|
|
@@ -969,10 +1084,21 @@ pub fn encode_docs_into(
|
|
|
969
1084
|
docs: &[&[u8]],
|
|
970
1085
|
dest: GatherBuf,
|
|
971
1086
|
) -> GatherOutcome {
|
|
1087
|
+
encode_docs_into_with(workers, proto, docs, dest, None).expect(UNCANCELLABLE)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/// `encode_docs_into` with a cancellation token (see `encode_chunks_gathered`).
|
|
1091
|
+
fn encode_docs_into_with(
|
|
1092
|
+
workers: &WorkerPool,
|
|
1093
|
+
proto: &Tokenizer,
|
|
1094
|
+
docs: &[&[u8]],
|
|
1095
|
+
dest: GatherBuf,
|
|
1096
|
+
cancel: Option<&AtomicBool>,
|
|
1097
|
+
) -> Option<GatherOutcome> {
|
|
972
1098
|
let total: usize = docs.iter().map(|d| d.len()).sum();
|
|
973
1099
|
let added = proto.added_token_split_blockers();
|
|
974
1100
|
let chunks = build_doc_chunks(docs, total, chunk_target_bytes(total), &added, lpt_from_env());
|
|
975
|
-
encode_chunks_into(workers, proto, &chunks, total, dest)
|
|
1101
|
+
encode_chunks_into(workers, proto, &chunks, total, dest, cancel)
|
|
976
1102
|
}
|
|
977
1103
|
|
|
978
1104
|
/// Sequential `encode_docs_ragged`: encode every document in order on the
|
|
@@ -1136,8 +1262,19 @@ pub fn encode_files_docs(
|
|
|
1136
1262
|
files: &[&[u8]],
|
|
1137
1263
|
format: &DocFormat,
|
|
1138
1264
|
) -> (Vec<u32>, Vec<i64>) {
|
|
1265
|
+
encode_files_docs_with(workers, proto, files, format, None).expect(UNCANCELLABLE)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/// `encode_files_docs` with a cancellation token (see `encode_chunks_gathered`).
|
|
1269
|
+
fn encode_files_docs_with(
|
|
1270
|
+
workers: &WorkerPool,
|
|
1271
|
+
proto: &Tokenizer,
|
|
1272
|
+
files: &[&[u8]],
|
|
1273
|
+
format: &DocFormat,
|
|
1274
|
+
cancel: Option<&AtomicBool>,
|
|
1275
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
1139
1276
|
if matches!(format, DocFormat::Text { separator: None }) {
|
|
1140
|
-
return
|
|
1277
|
+
return encode_docs_ragged_with(workers, proto, files, lpt_from_env(), cancel);
|
|
1141
1278
|
}
|
|
1142
1279
|
let total: usize = files.iter().map(|f| f.len()).sum();
|
|
1143
1280
|
let target = chunk_target_bytes(total);
|
|
@@ -1152,7 +1289,7 @@ pub fn encode_files_docs(
|
|
|
1152
1289
|
})
|
|
1153
1290
|
})
|
|
1154
1291
|
.collect();
|
|
1155
|
-
encode_chunks_gathered(workers, proto, &chunks, total)
|
|
1292
|
+
encode_chunks_gathered(workers, proto, &chunks, total, cancel)
|
|
1156
1293
|
}
|
|
1157
1294
|
|
|
1158
1295
|
/// Sequential `encode_files_docs`: extract and encode every document in
|
|
@@ -1165,6 +1302,19 @@ pub fn encode_files_docs_serial(
|
|
|
1165
1302
|
files: &[&[u8]],
|
|
1166
1303
|
format: &DocFormat,
|
|
1167
1304
|
) -> (Vec<u32>, Vec<i64>) {
|
|
1305
|
+
encode_files_docs_serial_with(workers, proto, files, format, None).expect(UNCANCELLABLE)
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/// `encode_files_docs_serial` with a cancellation token, polled per
|
|
1309
|
+
/// document — this path has no chunks to stop between (see
|
|
1310
|
+
/// `encode_chunks_gathered`).
|
|
1311
|
+
fn encode_files_docs_serial_with(
|
|
1312
|
+
workers: &WorkerPool,
|
|
1313
|
+
proto: &Tokenizer,
|
|
1314
|
+
files: &[&[u8]],
|
|
1315
|
+
format: &DocFormat,
|
|
1316
|
+
cancel: Option<&AtomicBool>,
|
|
1317
|
+
) -> Option<(Vec<u32>, Vec<i64>)> {
|
|
1168
1318
|
let total: usize = files.iter().map(|f| f.len()).sum();
|
|
1169
1319
|
workers.with_serial_worker(proto, total, |tok| {
|
|
1170
1320
|
let mut ids: Vec<u32> = Vec::with_capacity(total / 4 + 16);
|
|
@@ -1173,9 +1323,16 @@ pub fn encode_files_docs_serial(
|
|
|
1173
1323
|
madvise_hugepage(ids.as_mut_ptr() as *mut u8, ids.capacity() * 4);
|
|
1174
1324
|
let mut lens = Vec::new();
|
|
1175
1325
|
for &bytes in files {
|
|
1176
|
-
for_each_doc(bytes, format, |doc|
|
|
1326
|
+
for_each_doc(bytes, format, |doc| {
|
|
1327
|
+
// Once cancelled the result is discarded, so stop encoding;
|
|
1328
|
+
// the walk itself is a cheap scan and runs out rather than
|
|
1329
|
+
// threading a break through `for_each_doc`'s callback.
|
|
1330
|
+
if !cancelled(cancel) {
|
|
1331
|
+
encode_into(tok, doc, &mut ids, &mut lens);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1177
1334
|
}
|
|
1178
|
-
(ids, lens)
|
|
1335
|
+
(!cancelled(cancel)).then_some((ids, lens))
|
|
1179
1336
|
})
|
|
1180
1337
|
}
|
|
1181
1338
|
|
|
@@ -1306,7 +1463,8 @@ mod tests {
|
|
|
1306
1463
|
// A fresh pool per shape so each run exercises the pre-sized
|
|
1307
1464
|
// fork (slots fork lazily on first use).
|
|
1308
1465
|
let workers = WorkerPool::new();
|
|
1309
|
-
let (flat, lens) =
|
|
1466
|
+
let (flat, lens) =
|
|
1467
|
+
encode_docs_ragged_with(&workers, &proto, &docs, lpt, None).expect(UNCANCELLABLE);
|
|
1310
1468
|
assert_eq!(lens, lens_ref, "lens mismatch (lpt={lpt})");
|
|
1311
1469
|
assert_eq!(flat, ids_ref, "ids mismatch (lpt={lpt})");
|
|
1312
1470
|
}
|
|
@@ -1673,7 +1831,8 @@ mod tests {
|
|
|
1673
1831
|
|
|
1674
1832
|
for lpt in [true, false] {
|
|
1675
1833
|
let workers = WorkerPool::new();
|
|
1676
|
-
let (flat, lens) =
|
|
1834
|
+
let (flat, lens) =
|
|
1835
|
+
encode_docs_ragged_with(&workers, &proto, &docs, lpt, None).expect(UNCANCELLABLE);
|
|
1677
1836
|
assert_eq!(lens, lens_ref, "lens mismatch (lpt={lpt})");
|
|
1678
1837
|
if flat != ids_ref {
|
|
1679
1838
|
let i = ids_ref
|
|
@@ -1723,14 +1882,16 @@ mod tests {
|
|
|
1723
1882
|
assert!(chunks.len() > 1, "test must exercise the parallel path");
|
|
1724
1883
|
|
|
1725
1884
|
let workers = WorkerPool::new();
|
|
1726
|
-
let (flat_ref, lens_ref) =
|
|
1885
|
+
let (flat_ref, lens_ref) =
|
|
1886
|
+
encode_chunks_gathered(&workers, &proto, &chunks, total, None).expect(UNCANCELLABLE);
|
|
1727
1887
|
// Byte-level vocab: one token per byte, so any cap below `total`
|
|
1728
1888
|
// overflows; total / 3 overflows mid-flight with a committed
|
|
1729
1889
|
// prefix behind it.
|
|
1730
1890
|
for cap in [0, 1, total / 3] {
|
|
1731
1891
|
let workers = WorkerPool::new();
|
|
1732
1892
|
let (flat, lens) =
|
|
1733
|
-
encode_chunks_gathered_with_cap(&workers, &proto, &chunks, total, cap)
|
|
1893
|
+
encode_chunks_gathered_with_cap(&workers, &proto, &chunks, total, cap, None)
|
|
1894
|
+
.expect(UNCANCELLABLE);
|
|
1734
1895
|
assert_eq!(lens, lens_ref, "lens mismatch (cap={cap})");
|
|
1735
1896
|
assert_eq!(flat, flat_ref, "ids mismatch (cap={cap})");
|
|
1736
1897
|
}
|
|
@@ -1767,7 +1928,8 @@ mod tests {
|
|
|
1767
1928
|
assert!(chunks.len() > 1, "test must exercise the parallel path");
|
|
1768
1929
|
|
|
1769
1930
|
let workers = WorkerPool::new();
|
|
1770
|
-
let (flat_ref, lens_ref) =
|
|
1931
|
+
let (flat_ref, lens_ref) =
|
|
1932
|
+
encode_chunks_gathered(&workers, &proto, &chunks, total, None).expect(UNCANCELLABLE);
|
|
1771
1933
|
|
|
1772
1934
|
// A full-size destination: every chunk commits straight into it.
|
|
1773
1935
|
let mut dest_buf = vec![0u32; total];
|
|
@@ -1775,7 +1937,8 @@ mod tests {
|
|
|
1775
1937
|
// SAFETY: `dest_buf` is `total` tokens, exclusively owned for the
|
|
1776
1938
|
// duration of this call.
|
|
1777
1939
|
let dest = unsafe { GatherBuf::new(dest_buf.as_mut_ptr(), total) };
|
|
1778
|
-
|
|
1940
|
+
let gathered = encode_chunks_into(&workers, &proto, &chunks, total, dest, None);
|
|
1941
|
+
match gathered.expect(UNCANCELLABLE) {
|
|
1779
1942
|
GatherOutcome::Committed(n, lens) => {
|
|
1780
1943
|
assert_eq!(lens, lens_ref, "lens mismatch (committed)");
|
|
1781
1944
|
assert_eq!(&dest_buf[..n], &flat_ref[..], "ids mismatch (committed)");
|
|
@@ -1792,7 +1955,8 @@ mod tests {
|
|
|
1792
1955
|
// SAFETY: `small_buf` is `cap` tokens, exclusively owned for the
|
|
1793
1956
|
// duration of this call.
|
|
1794
1957
|
let dest = unsafe { GatherBuf::new(small_buf.as_mut_ptr(), cap) };
|
|
1795
|
-
|
|
1958
|
+
let gathered = encode_chunks_into(&workers, &proto, &chunks, total, dest, None);
|
|
1959
|
+
match gathered.expect(UNCANCELLABLE) {
|
|
1796
1960
|
GatherOutcome::Fallback(flat, lens) => {
|
|
1797
1961
|
assert_eq!(lens, lens_ref, "lens mismatch (fallback)");
|
|
1798
1962
|
assert_eq!(flat, flat_ref, "ids mismatch (fallback)");
|
|
@@ -1800,4 +1964,78 @@ mod tests {
|
|
|
1800
1964
|
GatherOutcome::Committed(..) => panic!("expected a too-small dest to overflow"),
|
|
1801
1965
|
}
|
|
1802
1966
|
}
|
|
1967
|
+
|
|
1968
|
+
/// Every entry point the Ruby extension cancels must report a cancelled
|
|
1969
|
+
/// run rather than hand back a batch missing its tail — and, with the
|
|
1970
|
+
/// token clear, must be identical to its uncancellable twin.
|
|
1971
|
+
#[test]
|
|
1972
|
+
fn cancelled_runs_return_nothing() {
|
|
1973
|
+
let merges = HashMap::with_hasher(rustc_hash::FxBuildHasher {});
|
|
1974
|
+
let vocab = (0..=u8::MAX).map(|b| vec![b]).collect();
|
|
1975
|
+
let proto = Tokenizer::new(merges, vocab, None);
|
|
1976
|
+
// Eight chunk-sized documents, so the parallel paths really do have
|
|
1977
|
+
// chunk boundaries to stop at (the lone-chunk shape every small
|
|
1978
|
+
// input takes is `cancelled_lone_chunk_returns_nothing`).
|
|
1979
|
+
let owned: Vec<Vec<u8>> = (0..8).map(|_| vec![b'a'; MIN_CHUNK_BYTES]).collect();
|
|
1980
|
+
let docs: Vec<&[u8]> = owned.iter().map(|d| d.as_slice()).collect();
|
|
1981
|
+
let total: usize = docs.iter().map(|d| d.len()).sum();
|
|
1982
|
+
let format = DocFormat::Text { separator: None };
|
|
1983
|
+
let workers = WorkerPool::new();
|
|
1984
|
+
|
|
1985
|
+
let reference = encode_docs_ragged(&workers, &proto, &docs);
|
|
1986
|
+
let cancel = AtomicBool::new(false);
|
|
1987
|
+
let uncancelled = [
|
|
1988
|
+
workers.encode_docs_ragged_cancellable(&proto, &docs, &cancel),
|
|
1989
|
+
workers.encode_files_docs_cancellable(&proto, &docs, &format, &cancel),
|
|
1990
|
+
workers.encode_files_docs_serial_cancellable(&proto, &docs, &format, &cancel),
|
|
1991
|
+
];
|
|
1992
|
+
for got in &uncancelled {
|
|
1993
|
+
assert!(got.as_ref() == Some(&reference), "uncancelled run differs");
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
cancel.store(true, Ordering::Relaxed);
|
|
1997
|
+
let mut buf = vec![0u32; total];
|
|
1998
|
+
// SAFETY: `buf` holds `total` tokens and is exclusively owned here.
|
|
1999
|
+
let dest = unsafe { GatherBuf::new(buf.as_mut_ptr(), total) };
|
|
2000
|
+
let ragged = workers.encode_docs_ragged_cancellable(&proto, &docs, &cancel);
|
|
2001
|
+
let into = workers.encode_docs_into_cancellable(&proto, &docs, dest, &cancel);
|
|
2002
|
+
let files = workers.encode_files_docs_cancellable(&proto, &docs, &format, &cancel);
|
|
2003
|
+
let serial = workers.encode_files_docs_serial_cancellable(&proto, &docs, &format, &cancel);
|
|
2004
|
+
assert!(ragged.is_none() && into.is_none() && files.is_none() && serial.is_none());
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/// The same contract for an input small enough to be one chunk — the
|
|
2008
|
+
/// shape every sub-MiB batch takes, and the one the cancellable entry
|
|
2009
|
+
/// points used to answer with a truncated result (I01 G1). The token is
|
|
2010
|
+
/// polled per document inside the chunk, so the run really does stop
|
|
2011
|
+
/// short; what it must not do is report that prefix as the batch.
|
|
2012
|
+
#[test]
|
|
2013
|
+
fn cancelled_lone_chunk_returns_nothing() {
|
|
2014
|
+
let merges = HashMap::with_hasher(rustc_hash::FxBuildHasher {});
|
|
2015
|
+
let vocab = (0..=u8::MAX).map(|b| vec![b]).collect();
|
|
2016
|
+
let proto = Tokenizer::new(merges, vocab, None);
|
|
2017
|
+
let owned: Vec<Vec<u8>> = (0..64).map(|i| vec![b'a' + (i % 26) as u8; 512]).collect();
|
|
2018
|
+
let docs: Vec<&[u8]> = owned.iter().map(|d| d.as_slice()).collect();
|
|
2019
|
+
let total: usize = docs.iter().map(|d| d.len()).sum();
|
|
2020
|
+
assert!(total < MIN_CHUNK_BYTES, "the input must be a single chunk");
|
|
2021
|
+
let format = DocFormat::Text { separator: None };
|
|
2022
|
+
let workers = WorkerPool::new();
|
|
2023
|
+
|
|
2024
|
+
let cancel = AtomicBool::new(true);
|
|
2025
|
+
let mut buf = vec![0u32; total];
|
|
2026
|
+
// SAFETY: `buf` holds `total` tokens and is exclusively owned here.
|
|
2027
|
+
let dest = unsafe { GatherBuf::new(buf.as_mut_ptr(), total) };
|
|
2028
|
+
let ragged = workers.encode_docs_ragged_cancellable(&proto, &docs, &cancel);
|
|
2029
|
+
let into = workers.encode_docs_into_cancellable(&proto, &docs, dest, &cancel);
|
|
2030
|
+
let files = workers.encode_files_docs_cancellable(&proto, &docs, &format, &cancel);
|
|
2031
|
+
let serial = workers.encode_files_docs_serial_cancellable(&proto, &docs, &format, &cancel);
|
|
2032
|
+
assert!(ragged.is_none() && into.is_none() && files.is_none() && serial.is_none());
|
|
2033
|
+
|
|
2034
|
+
// With the token clear, the same lone chunk is the uncancellable
|
|
2035
|
+
// path's result exactly.
|
|
2036
|
+
let cancel = AtomicBool::new(false);
|
|
2037
|
+
let reference = encode_docs_ragged(&workers, &proto, &docs);
|
|
2038
|
+
let got = workers.encode_docs_ragged_cancellable(&proto, &docs, &cancel);
|
|
2039
|
+
assert!(got.as_ref() == Some(&reference), "lone chunk differs");
|
|
2040
|
+
}
|
|
1803
2041
|
}
|
data/src/bpe/tiktoken.rs
CHANGED
|
@@ -12,7 +12,7 @@ use crate::pretokenize::{
|
|
|
12
12
|
Pretoken, PretokenSpans, PretokenizerType, SpanBatch, pack_pretoken_key, pretoken_key_hash,
|
|
13
13
|
};
|
|
14
14
|
use crate::token::TokenId;
|
|
15
|
-
use eyre::Result;
|
|
15
|
+
use eyre::{Result, ensure, eyre};
|
|
16
16
|
use std::collections::HashMap;
|
|
17
17
|
use std::fmt::{Debug, Formatter};
|
|
18
18
|
use std::sync::Arc;
|
|
@@ -708,6 +708,12 @@ impl Tokenizer {
|
|
|
708
708
|
/// merges map and returns a Tokenizer.
|
|
709
709
|
///
|
|
710
710
|
/// This process is necessary to load some tokenizers found in tiktoken.
|
|
711
|
+
///
|
|
712
|
+
/// A rank file is untrusted input, so the two shapes that make the
|
|
713
|
+
/// reconstruction impossible are errors rather than panics: a multi-byte
|
|
714
|
+
/// token whose bytes have no single-byte tokens to build from, and one
|
|
715
|
+
/// that does not reduce to a pair of tokens already seen (the merge it
|
|
716
|
+
/// would record has no two operands).
|
|
711
717
|
pub fn from_ranks(vocab: Vec<Vec<u8>>) -> Result<Self> {
|
|
712
718
|
let mut merges: HashMap<(TokenId, TokenId), TokenId, rustc_hash::FxBuildHasher> =
|
|
713
719
|
HashMap::with_hasher(rustc_hash::FxBuildHasher {});
|
|
@@ -727,10 +733,21 @@ impl Tokenizer {
|
|
|
727
733
|
}
|
|
728
734
|
let byte_symbols: Vec<u8> = token_bytes
|
|
729
735
|
.iter()
|
|
730
|
-
.map(|b|
|
|
731
|
-
|
|
736
|
+
.map(|b| {
|
|
737
|
+
vocab_inv
|
|
738
|
+
.get(std::slice::from_ref(b))
|
|
739
|
+
.map(|id| id.0 as u8)
|
|
740
|
+
.ok_or_else(|| {
|
|
741
|
+
eyre!("rank {token_idx}: no single-byte token for byte {b:#04x}")
|
|
742
|
+
})
|
|
743
|
+
})
|
|
744
|
+
.collect::<Result<_>>()?;
|
|
732
745
|
let tokenized = simple_bpe_merge(&merges, &byte_symbols);
|
|
733
|
-
|
|
746
|
+
ensure!(
|
|
747
|
+
tokenized.len() == 2,
|
|
748
|
+
"rank {token_idx}: reduces to {} tokens, not a mergeable pair",
|
|
749
|
+
tokenized.len()
|
|
750
|
+
);
|
|
734
751
|
merges.insert((tokenized[0], tokenized[1]), TokenId::from(token_idx));
|
|
735
752
|
}
|
|
736
753
|
|
|
@@ -1694,9 +1711,17 @@ impl Tokenizer {
|
|
|
1694
1711
|
}
|
|
1695
1712
|
}
|
|
1696
1713
|
|
|
1714
|
+
/// Concatenate the vocabulary entries of `v`. An id at or past
|
|
1715
|
+
/// [`Self::vocab_size`] contributes nothing rather than indexing out of
|
|
1716
|
+
/// bounds — a boundary that accepts ids from outside (the Ruby
|
|
1717
|
+
/// extension's `decode`) rejects them before getting here, and this is
|
|
1718
|
+
/// what keeps an unchecked one from taking the process down with it.
|
|
1697
1719
|
pub fn decode(&self, v: &[TokenId]) -> impl Iterator<Item = u8> {
|
|
1698
1720
|
v.iter()
|
|
1699
|
-
.flat_map(|&token| self.vocab
|
|
1721
|
+
.flat_map(|&token| match self.vocab.get(token.0 as usize) {
|
|
1722
|
+
Some(bytes) => bytes.as_ref(),
|
|
1723
|
+
None => &[],
|
|
1724
|
+
})
|
|
1700
1725
|
.copied()
|
|
1701
1726
|
}
|
|
1702
1727
|
|