#![cfg(feature = "vocab-llama3")]
use splintr::core::byte_pair_encode;
use splintr::pretrained::{llama3_special_tokens, LLAMA3_VOCAB_PACKED};
use splintr::{FxHashMap, Tokenizer, LLAMA3_PATTERN, NO_SPLIT_PATTERN};
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::sync::LazyLock;
use std::time::Instant;
thread_local! {
static ALLOCATIONS: Cell<u64> = const { Cell::new(0) };
static COUNTING: Cell<bool> = const { Cell::new(false) };
}
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let _ = COUNTING.try_with(|on| {
if on.get() {
let _ = ALLOCATIONS.try_with(|n| n.set(n.get() + 1));
}
});
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let _ = COUNTING.try_with(|on| {
if on.get() {
let _ = ALLOCATIONS.try_with(|n| n.set(n.get() + 1));
}
});
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
fn allocations_of<T>(f: impl FnOnce() -> T) -> u64 {
ALLOCATIONS.with(|n| n.set(0));
COUNTING.with(|on| on.set(true));
let out = f();
COUNTING.with(|on| on.set(false));
drop(out);
ALLOCATIONS.with(Cell::get)
}
static TOKENIZER: LazyLock<Tokenizer> = LazyLock::new(|| {
Tokenizer::from_packed_chain(
LLAMA3_VOCAB_PACKED,
&[LLAMA3_PATTERN],
llama3_special_tokens(),
)
.expect("bundled llama3 vocabulary must load")
});
static UNSPLIT: LazyLock<Tokenizer> = LazyLock::new(|| {
Tokenizer::from_packed_chain(
LLAMA3_VOCAB_PACKED,
&[NO_SPLIT_PATTERN],
llama3_special_tokens(),
)
.expect("bundled llama3 vocabulary must load under a no-split pattern")
});
fn novel_words(count: usize, len: usize, seed: u64) -> Vec<String> {
let alphabet = b"abcdefghijklmnopqrstuvwxyz";
let mut state = seed | 1;
let mut next = move || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) as usize
};
(0..count)
.map(|_| {
(0..len)
.map(|_| alphabet[next() % alphabet.len()] as char)
.collect()
})
.collect()
}
fn chaining_encoder() -> splintr::core::Encoder {
let mut encoder = FxHashMap::default();
let alphabet = *b"abcd";
for (i, &b) in alphabet.iter().enumerate() {
encoder.insert(vec![b], i as u32);
}
let mut next = alphabet.len() as u32;
for &b in &alphabet {
for width in [2usize, 4, 8, 16, 32] {
encoder.insert(vec![b; width], next);
next += 1;
}
}
for &l in &alphabet {
for &r in &alphabet {
encoder.insert(vec![l, r], next);
next += 1;
}
}
splintr::core::encoder_from_owned(encoder)
}
#[test]
fn merge_allocation_count_does_not_grow_with_piece_length() {
let encoder = chaining_encoder();
let short = b"abcdab".to_vec();
let long = b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd".to_vec();
let _ = byte_pair_encode(b"aa", &encoder);
let _ = byte_pair_encode(&long, &encoder);
let short_allocs = allocations_of(|| byte_pair_encode(&short, &encoder));
let long_allocs = allocations_of(|| byte_pair_encode(&long, &encoder));
assert_eq!(
long_allocs,
short_allocs,
"merging a {}-byte piece allocated {long_allocs} times against {short_allocs} for a \
{}-byte piece: selection allocates as it queues candidates, so every additional merge \
costs a malloc",
long.len(),
short.len()
);
}
#[test]
fn merging_one_piece_allocates_a_small_constant_number_of_times() {
let encoder = chaining_encoder();
let piece = b"abcdabcdabcdabcdabcdabcd".to_vec();
let _ = byte_pair_encode(b"aa", &encoder);
let allocs = allocations_of(|| byte_pair_encode(&piece, &encoder));
assert!(
allocs <= 3,
"merging one {}-byte piece allocated {allocs} times; the algorithm needs the node list \
and the output vector, so anything beyond that is per-merge allocation",
piece.len()
);
}
#[test]
fn unsplit_input_encodes_sub_quadratically() {
let tokenizer = &*UNSPLIT;
let base: String = novel_words(400, 5, 0x3311).join(" ");
let small: String = base.chars().take(1000).collect();
let large: String = base.chars().take(4000).collect();
tokenizer.encode_ordinary("warmup");
let time = |text: &str| {
let start = Instant::now();
std::hint::black_box(tokenizer.encode_ordinary(text));
start.elapsed().as_secs_f64()
};
let small_secs = time(&small);
let large_secs = time(&large);
let ratio = large_secs / small_secs;
assert!(
ratio < 8.0,
"4x the input took {ratio:.1}x the time (1000 chars: {:.3}ms, 4000 chars: {:.3}ms); \
merge selection is quadratic in the length of an unsplit piece",
small_secs * 1000.0,
large_secs * 1000.0
);
}
#[cfg(feature = "rayon")]
#[test]
fn batch_encoding_scales_with_available_cores() {
let cores = std::thread::available_parallelism().map_or(1, |n| n.get());
if cores < 4 {
eprintln!("skipped: needs >= 4 cores, found {cores}");
return;
}
let tokenizer = &*TOKENIZER;
let texts: Vec<String> = (0..512)
.map(|i| novel_words(24, 7, 0x9E37 ^ (i as u64)).join(" "))
.collect();
std::hint::black_box(tokenizer.encode_batch(&texts));
let best = |mut f: Box<dyn FnMut()>| {
(0..5)
.map(|_| {
let start = Instant::now();
f();
start.elapsed().as_secs_f64()
})
.fold(f64::INFINITY, f64::min)
};
let sequential = best(Box::new(|| {
for text in &texts {
std::hint::black_box(tokenizer.encode(text));
}
}));
let parallel = best(Box::new(|| {
std::hint::black_box(tokenizer.encode_batch(&texts));
}));
let speedup = sequential / parallel;
assert!(
speedup >= 1.3,
"encode_batch was only {speedup:.2}x the sequential loop on {cores} cores ({:.3}ms \
against {:.3}ms): the parallel path is contending on shared per-chunk state instead \
of overlapping work",
parallel * 1000.0,
sequential * 1000.0
);
}