1use std::{
27 hash::BuildHasherDefault,
28 mem::size_of_val,
29 sync::{
30 Arc,
31 atomic::{AtomicU64, Ordering},
32 },
33};
34
35use aho_corasick::AhoCorasick;
36use moka::sync::Cache;
37use rustc_hash::FxHasher;
38
39use crate::{TokenIdType, traits::Encoder};
40
41type Blake3Hash = [u8; 32];
43
44type PrefixHasher = BuildHasherDefault<FxHasher>;
47
48type PrefixCache = Cache<Blake3Hash, Arc<[TokenIdType]>, PrefixHasher>;
50
51fn boundaries_with(text: &str, matcher: &AhoCorasick) -> Vec<usize> {
60 let mut boundaries: Vec<usize> = matcher
61 .find_overlapping_iter(text)
62 .map(|m| m.end())
63 .filter(|&end| end < text.len())
64 .collect();
65 boundaries.sort_unstable();
66 boundaries.dedup();
67 boundaries
68}
69
70fn has_nontrivial_self_overlap(token: &str) -> bool {
71 let bytes = token.as_bytes();
72 (1..bytes.len()).any(|overlap| bytes[bytes.len() - overlap..] == bytes[..overlap])
73}
74
75fn tokens_can_overlap(a: &str, b: &str) -> bool {
76 if a.contains(b) || b.contains(a) {
77 return true;
78 }
79
80 let a = a.as_bytes();
81 let b = b.as_bytes();
82 let max_overlap = a.len().min(b.len());
83 (1..max_overlap).any(|overlap| {
84 a[a.len() - overlap..] == b[..overlap] || b[b.len() - overlap..] == a[..overlap]
85 })
86}
87
88pub(super) fn first_unsafe_overlap(special_tokens: &[String]) -> Option<(&str, &str)> {
96 for (index, token) in special_tokens.iter().enumerate() {
97 if token.is_empty() {
98 continue;
99 }
100 if has_nontrivial_self_overlap(token) {
101 return Some((token, token));
102 }
103 for other in &special_tokens[index + 1..] {
104 if !other.is_empty() && token != other && tokens_can_overlap(token, other) {
105 return Some((token, other));
106 }
107 }
108 }
109
110 None
111}
112
113#[cfg(test)]
116fn find_special_token_boundaries(text: &str, special_tokens: &[&str]) -> Vec<usize> {
117 if special_tokens.is_empty() {
118 return Vec::new();
119 }
120 let matcher = AhoCorasick::new(special_tokens)
121 .expect("special tokens form a valid Aho-Corasick automaton");
122 boundaries_with(text, &matcher)
123}
124
125pub type CacheEventFn = Arc<dyn Fn() + Send + Sync>;
129
130pub struct L1Cache {
134 cache: PrefixCache,
136 matcher: Option<AhoCorasick>,
139 hits: AtomicU64,
140 misses: AtomicU64,
141 on_hit: Option<CacheEventFn>,
142 on_miss: Option<CacheEventFn>,
143}
144
145impl L1Cache {
146 pub fn new(max_memory: usize, mut special_tokens: Vec<String>) -> Self {
149 special_tokens.retain(|token| !token.is_empty());
150
151 let cache = Cache::builder()
155 .max_capacity(max_memory as u64)
156 .weigher(|_k: &Blake3Hash, tokens: &Arc<[TokenIdType]>| -> u32 {
157 size_of_val(tokens.as_ref()).min(u32::MAX as usize) as u32
158 })
159 .build_with_hasher(PrefixHasher::default());
160
161 let matcher = (!special_tokens.is_empty()).then(|| {
163 AhoCorasick::new(&special_tokens)
164 .expect("special tokens form a valid Aho-Corasick automaton")
165 });
166
167 Self {
168 cache,
169 matcher,
170 hits: AtomicU64::new(0),
171 misses: AtomicU64::new(0),
172 on_hit: None,
173 on_miss: None,
174 }
175 }
176
177 pub fn set_observer(&mut self, on_hit: CacheEventFn, on_miss: CacheEventFn) {
179 self.on_hit = Some(on_hit);
180 self.on_miss = Some(on_miss);
181 }
182
183 fn boundaries(&self, text: &str) -> Vec<usize> {
187 match &self.matcher {
188 Some(matcher) => boundaries_with(text, matcher),
189 None => Vec::new(),
190 }
191 }
192
193 pub fn longest_prefix_match(&self, input: &str) -> Option<(Arc<[TokenIdType]>, usize, usize)> {
200 let boundaries = self.boundaries(input);
201
202 if boundaries.is_empty() {
203 self.misses.fetch_add(1, Ordering::Relaxed);
204 if let Some(cb) = &self.on_miss {
205 cb();
206 }
207 return None;
208 }
209
210 let deepest_boundary = *boundaries.last().expect("boundaries is non-empty here");
213
214 let mut hasher = blake3::Hasher::new();
216 let mut prefix_hashes = Vec::with_capacity(boundaries.len());
217 let mut last_pos = 0;
218 let bytes = input.as_bytes();
219 for &boundary_pos in &boundaries {
220 hasher.update(&bytes[last_pos..boundary_pos]);
221 prefix_hashes.push((boundary_pos, *hasher.finalize().as_bytes()));
223 last_pos = boundary_pos;
224 }
225
226 for (boundary_pos, hash_bytes) in prefix_hashes.into_iter().rev() {
229 if let Some(tokens) = self.cache.get(&hash_bytes) {
230 self.hits.fetch_add(1, Ordering::Relaxed);
231 if let Some(cb) = &self.on_hit {
232 cb();
233 }
234 return Some((tokens, boundary_pos, deepest_boundary));
238 }
239 }
240
241 self.misses.fetch_add(1, Ordering::Relaxed);
242 if let Some(cb) = &self.on_miss {
243 cb();
244 }
245 None
246 }
247
248 pub fn insert_at_boundaries<E: Encoder + ?Sized>(
256 &self,
257 input: &str,
258 tokenizer: &E,
259 ) -> anyhow::Result<()> {
260 let boundaries = self.boundaries(input);
261 if boundaries.is_empty() {
262 return Ok(());
263 }
264 self.populate_boundaries(input, &boundaries, tokenizer)?;
265 Ok(())
266 }
267
268 pub fn populate_and_encode<E: Encoder + ?Sized>(
277 &self,
278 input: &str,
279 tokenizer: &E,
280 ) -> anyhow::Result<Vec<TokenIdType>> {
281 let boundaries = self.boundaries(input);
282 if boundaries.is_empty() {
283 return Ok(tokenizer.encode(input)?.token_ids().to_vec());
285 }
286
287 let mut running = self.populate_boundaries(input, &boundaries, tokenizer)?;
289
290 let tail_start = *boundaries.last().expect("boundaries is non-empty here");
293 let tail = tokenizer.encode(&input[tail_start..])?;
294 running.extend_from_slice(tail.token_ids());
295 Ok(running)
296 }
297
298 fn populate_boundaries<E: Encoder + ?Sized>(
302 &self,
303 input: &str,
304 boundaries: &[usize],
305 tokenizer: &E,
306 ) -> anyhow::Result<Vec<TokenIdType>> {
307 let mut hasher = blake3::Hasher::new();
308 let mut running_tokens: Vec<TokenIdType> = Vec::new();
309 let mut last_pos = 0;
310 let bytes = input.as_bytes();
311
312 for &boundary_pos in boundaries {
313 hasher.update(&bytes[last_pos..boundary_pos]);
315 let hash_bytes: Blake3Hash = *hasher.finalize().as_bytes();
316
317 let seg = tokenizer.encode(&input[last_pos..boundary_pos])?;
321 running_tokens.extend_from_slice(seg.token_ids());
322
323 let prefix_tokens: Arc<[TokenIdType]> = running_tokens.as_slice().into();
326 self.cache.insert(hash_bytes, prefix_tokens);
327
328 last_pos = boundary_pos;
329 }
330
331 Ok(running_tokens)
332 }
333
334 pub fn extend_after_match<E: Encoder + ?Sized>(
351 &self,
352 input: &str,
353 prefix_tokens: Arc<[TokenIdType]>,
354 prefix_len: usize,
355 deepest_boundary: usize,
356 tokenizer: &E,
357 ) -> anyhow::Result<Vec<TokenIdType>> {
358 let deepest = (deepest_boundary > prefix_len).then_some(deepest_boundary);
364
365 let Some(deepest) = deepest else {
366 let suffix_enc = tokenizer.encode(&input[prefix_len..])?;
370 let mut merged = Vec::with_capacity(prefix_tokens.len() + suffix_enc.token_ids().len());
371 merged.extend_from_slice(&prefix_tokens);
372 merged.extend_from_slice(suffix_enc.token_ids());
373 return Ok(merged);
374 };
375
376 let seg_a = tokenizer.encode(&input[prefix_len..deepest])?;
383 let seg_b = tokenizer.encode(&input[deepest..])?;
384 let mut cumulative = Vec::with_capacity(
385 prefix_tokens.len() + seg_a.token_ids().len() + seg_b.token_ids().len(),
386 );
387 cumulative.extend_from_slice(&prefix_tokens);
388 cumulative.extend_from_slice(seg_a.token_ids());
389
390 let mut hasher = blake3::Hasher::new();
394 hasher.update(&input.as_bytes()[..deepest]);
395 let hash_bytes: Blake3Hash = *hasher.finalize().as_bytes();
396
397 let tokens: Arc<[TokenIdType]> = cumulative.as_slice().into();
400 self.cache.insert(hash_bytes, tokens);
401
402 cumulative.extend_from_slice(seg_b.token_ids());
405 Ok(cumulative)
406 }
407
408 pub fn len(&self) -> usize {
411 self.cache.run_pending_tasks();
412 self.cache.entry_count() as usize
413 }
414
415 pub fn is_empty(&self) -> bool {
416 self.len() == 0
417 }
418
419 pub fn stats(&self) -> L1CacheStats {
420 self.cache.run_pending_tasks();
422 let hits = self.hits.load(Ordering::Relaxed);
423 let misses = self.misses.load(Ordering::Relaxed);
424 let total_requests = hits + misses;
425
426 L1CacheStats {
427 hits,
428 misses,
429 entries: self.cache.entry_count() as usize,
430 memory_bytes: self.cache.weighted_size() as usize,
431 hit_rate: if total_requests > 0 {
432 hits as f64 / total_requests as f64
433 } else {
434 0.0
435 },
436 }
437 }
438
439 pub fn clear(&self) {
440 self.cache.invalidate_all();
441 self.cache.run_pending_tasks();
442 self.hits.store(0, Ordering::Relaxed);
443 self.misses.store(0, Ordering::Relaxed);
444 }
445}
446
447#[derive(Debug, Clone)]
448pub struct L1CacheStats {
449 pub hits: u64,
450 pub misses: u64,
451 pub entries: usize,
452 pub memory_bytes: usize,
453 pub hit_rate: f64,
454}
455
456#[cfg(test)]
457mod tests {
458 use std::sync::Arc;
459
460 use super::*;
461 use crate::{HuggingFaceTokenizer, traits::Tokenizer};
462
463 const TINYLLAMA_PATH: &str = concat!(
466 env!("CARGO_MANIFEST_DIR"),
467 "/tests/data/sample-models/TinyLlama_v1.1/tokenizer.json"
468 );
469
470 const SPECIALS: &[&str] = &["<s>", "</s>"];
471
472 fn load_tokenizer() -> Arc<dyn Tokenizer> {
473 Arc::new(HuggingFaceTokenizer::from_file(TINYLLAMA_PATH).expect("load TinyLlama"))
474 }
475
476 fn test_cache(max_memory: usize) -> L1Cache {
478 L1Cache::new(
479 max_memory,
480 SPECIALS.iter().map(|s| (*s).to_string()).collect(),
481 )
482 }
483
484 #[test]
485 fn boundaries_are_after_each_special_token_occurrence() {
486 let input = "<s>system\nHi</s><s>user\nHello</s>";
487 let bounds = find_special_token_boundaries(input, SPECIALS);
488 assert_eq!(bounds.len(), 3);
490 for w in bounds.windows(2) {
491 assert!(w[0] < w[1], "boundaries must be strictly increasing");
492 }
493 assert!(bounds.iter().all(|&b| b < input.len()));
494 }
495
496 #[test]
497 fn no_special_tokens_yields_no_boundaries() {
498 assert!(find_special_token_boundaries("plain text", &[]).is_empty());
499 }
500
501 #[test]
502 fn unsafe_overlap_detects_containment_crossing_and_self_overlap() {
503 let cases = [
504 (vec!["〈|", "〈|EOS|〉"], Some(("〈|", "〈|EOS|〉"))),
505 (vec!["ab", "bc"], Some(("ab", "bc"))),
506 (vec!["|◊|"], Some(("|◊|", "|◊|"))),
507 (vec!["<s>", "<s>"], None),
508 ];
509
510 for (tokens, expected) in cases {
511 let tokens: Vec<String> = tokens.into_iter().map(String::from).collect();
512 assert_eq!(first_unsafe_overlap(&tokens), expected);
513 }
514 }
515
516 #[test]
517 fn llama_numbered_special_tokens_do_not_trigger_overlap_guard() {
518 let mut llama: Vec<String> = [
519 "<|begin_of_text|>",
520 "<|end_of_text|>",
521 "<|start_header_id|>",
522 "<|end_header_id|>",
523 "<|eot_id|>",
524 ]
525 .into_iter()
526 .map(String::from)
527 .collect();
528 llama.extend((0..251).map(|id| format!("<|reserved_special_token_{id}|>")));
529
530 assert_eq!(first_unsafe_overlap(&llama), None);
531 }
532
533 #[test]
534 fn insert_then_lookup_finds_shared_prefix() {
535 let cache = test_cache(1024 * 1024);
536 let tokenizer = load_tokenizer();
537
538 let warm = "<s>system\nYou are helpful.</s><s>user\nHi</s>";
539 cache
540 .insert_at_boundaries(warm, tokenizer.as_ref())
541 .unwrap();
542 assert!(!cache.is_empty());
543
544 let target = "<s>system\nYou are helpful.</s><s>user\nDifferent question</s>";
545 let (tokens, offset, _deepest) = cache
546 .longest_prefix_match(target)
547 .expect("shared prefix should match");
548 assert!(offset > 0);
549 assert!(!tokens.is_empty());
550 }
551
552 #[test]
553 fn miss_increments_misses_counter() {
554 let cache = test_cache(1024 * 1024);
555 assert!(
556 cache
557 .longest_prefix_match("plain text no specials")
558 .is_none()
559 );
560 assert_eq!(cache.stats().misses, 1);
561 }
562
563 #[test]
564 fn hit_increments_hits_counter() {
565 let cache = test_cache(1024 * 1024);
566 let tokenizer = load_tokenizer();
567 let warm = "<s>system\nA.</s><s>user\nB</s>";
568 cache
569 .insert_at_boundaries(warm, tokenizer.as_ref())
570 .unwrap();
571 let _ = cache.longest_prefix_match(warm);
572 assert!(cache.stats().hits >= 1);
573 }
574
575 #[test]
576 fn merge_invariant_holds_against_uncached_encode() {
577 let cache = test_cache(1024 * 1024);
581 let tokenizer = load_tokenizer();
582
583 let template = "<s>system\nYou are helpful.</s><s>user\n";
584 let warm = format!("{template}First.</s>");
585 cache
586 .insert_at_boundaries(&warm, tokenizer.as_ref())
587 .unwrap();
588
589 let target = format!("{template}A completely different second question.</s>");
590 let (prefix_tokens, prefix_len, _deepest) = cache
591 .longest_prefix_match(&target)
592 .expect("should find prefix");
593
594 let suffix = &target[prefix_len..];
595 let suffix_enc = tokenizer.encode(suffix).unwrap();
596 let mut merged = prefix_tokens.to_vec();
598 merged.extend_from_slice(suffix_enc.token_ids());
599
600 let plain = tokenizer.encode(&target).unwrap();
601 assert_eq!(
602 merged,
603 plain.token_ids(),
604 "merged tokens must equal plain encode"
605 );
606 }
607
608 #[test]
609 fn eviction_respects_memory_budget() {
610 let cache = test_cache(4 * 1024);
612 let tokenizer = load_tokenizer();
613 for i in 0..50 {
614 let input =
615 format!("<s>system\nPersona {i} chatty.</s><s>user\nTurn {i} content here.</s>");
616 cache
617 .insert_at_boundaries(&input, tokenizer.as_ref())
618 .unwrap();
619 }
620 let stats = cache.stats();
621 assert!(
622 stats.memory_bytes <= 4 * 1024,
623 "memory_bytes={} exceeds budget",
624 stats.memory_bytes
625 );
626 }
627
628 #[test]
629 fn concurrent_inserts_and_lookups_do_not_corrupt() {
630 use std::thread;
631
632 let cache = Arc::new(test_cache(1024 * 1024));
633 let tokenizer = load_tokenizer();
634
635 let mut handles = vec![];
636 for i in 0..10 {
637 let cache_c = cache.clone();
638 let tok = tokenizer.clone();
639 handles.push(thread::spawn(move || {
640 let input = format!("<s>system\nThread {i}.</s><s>user\nThread {i} body.</s>");
641 cache_c.insert_at_boundaries(&input, tok.as_ref()).unwrap();
642 let r = cache_c.longest_prefix_match(&input);
643 assert!(r.is_some(), "thread {i} expected match after insert");
644 }));
645 }
646 for h in handles {
647 h.join().unwrap();
648 }
649 assert!(cache.stats().memory_bytes > 0);
650 assert!(cache.stats().hits >= 10);
651 }
652
653 fn growing_chat_turns(n: usize) -> Vec<String> {
659 let mut convo = String::from("<s>system\nYou are a helpful assistant.</s>");
660 let mut turns = Vec::with_capacity(n);
661 for i in 0..n {
662 convo.push_str(&format!(
663 "<s>user\nQuestion {i} please answer it.</s><s>assistant\nDetailed answer {i} follows here.</s>"
664 ));
665 turns.push(format!("{convo}<s>user\nFollow-up {i}"));
666 }
667 turns
668 }
669
670 #[test]
671 fn extend_on_hit_advances_match_depth_each_turn() {
672 let tok = load_tokenizer();
676 let turns = growing_chat_turns(5);
677
678 let off = test_cache(8 * 1024 * 1024);
680 off.insert_at_boundaries(&turns[0], tok.as_ref()).unwrap();
681 let pinned = off.longest_prefix_match(&turns[1]).expect("hit").1;
682 for t in &turns[1..] {
683 let (_toks, offset, _deepest) = off.longest_prefix_match(t).expect("hit");
684 assert_eq!(
685 offset, pinned,
686 "extend-off offset must stay pinned at turn-1 depth"
687 );
688 }
689
690 let on = test_cache(8 * 1024 * 1024);
692 on.insert_at_boundaries(&turns[0], tok.as_ref()).unwrap();
693 let mut prev = 0usize;
694 for (i, t) in turns.iter().enumerate().skip(1) {
695 let (prefix_tokens, offset, deepest) = on.longest_prefix_match(t).expect("hit");
696 assert!(
697 offset > prev,
698 "turn {i}: extend-on offset {offset} must exceed previous {prev}"
699 );
700 prev = offset;
701
702 let merged = on
704 .extend_after_match(t, prefix_tokens, offset, deepest, tok.as_ref())
705 .unwrap();
706 let plain = tok.encode(t).unwrap();
707 assert_eq!(
708 merged,
709 plain.token_ids(),
710 "turn {i}: extend merge must equal plain encode"
711 );
712 }
713
714 assert!(
715 prev > pinned,
716 "extend-on frontier ({prev}) must reach deeper than pinned extend-off depth ({pinned})"
717 );
718 }
719
720 #[test]
721 fn extend_on_hit_respects_budget_and_stays_correct() {
722 let tok = load_tokenizer();
725 let cache = test_cache(4 * 1024);
726 let turns = growing_chat_turns(20);
727 cache.insert_at_boundaries(&turns[0], tok.as_ref()).unwrap();
728
729 for t in &turns[1..] {
730 let merged = match cache.longest_prefix_match(t) {
731 Some((prefix_tokens, offset, deepest)) => cache
732 .extend_after_match(t, prefix_tokens, offset, deepest, tok.as_ref())
733 .unwrap(),
734 None => {
735 let enc = tok.encode(t).unwrap();
737 cache.insert_at_boundaries(t, tok.as_ref()).unwrap();
738 enc.token_ids().to_vec()
739 }
740 };
741 let plain = tok.encode(t).unwrap();
742 assert_eq!(
743 merged,
744 plain.token_ids(),
745 "encode must stay correct under eviction pressure"
746 );
747 assert!(
748 cache.stats().memory_bytes <= 4 * 1024,
749 "memory_bytes={} exceeds budget",
750 cache.stats().memory_bytes
751 );
752 }
753 }
754
755 #[test]
756 fn concurrent_extend_on_hit_does_not_corrupt() {
757 use std::thread;
758
759 let tok = load_tokenizer();
760 let cache = Arc::new(test_cache(8 * 1024 * 1024));
761 let turns = growing_chat_turns(8);
762 cache.insert_at_boundaries(&turns[0], tok.as_ref()).unwrap();
764
765 let mut handles = vec![];
766 for _ in 0..8 {
767 let cache_c = cache.clone();
768 let tok_c = tok.clone();
769 let turns_c = turns.clone();
770 handles.push(thread::spawn(move || {
771 for t in &turns_c[1..] {
772 if let Some((prefix_tokens, offset, deepest)) = cache_c.longest_prefix_match(t)
773 {
774 let merged = cache_c
775 .extend_after_match(t, prefix_tokens, offset, deepest, tok_c.as_ref())
776 .unwrap();
777 let plain = tok_c.encode(t).unwrap();
778 assert_eq!(
779 merged,
780 plain.token_ids(),
781 "concurrent extend must stay correct"
782 );
783 }
784 }
785 }));
786 }
787 for h in handles {
788 h.join().unwrap();
789 }
790 assert!(cache.stats().memory_bytes > 0);
791 }
792
793 #[test]
794 fn extend_after_match_persists_correct_deepest_entry() {
795 let tok = load_tokenizer();
801 let turns = growing_chat_turns(3);
802
803 let cache = test_cache(8 * 1024 * 1024);
804 cache.insert_at_boundaries(&turns[0], tok.as_ref()).unwrap();
805
806 let (prefix_tokens, prefix_len, deepest_boundary) = cache
807 .longest_prefix_match(&turns[1])
808 .expect("partial hit on turns[1]");
809 let entries_before = cache.stats().entries;
810
811 let _merged = cache
812 .extend_after_match(
813 &turns[1],
814 prefix_tokens,
815 prefix_len,
816 deepest_boundary,
817 tok.as_ref(),
818 )
819 .unwrap();
820
821 assert_eq!(
822 cache.stats().entries,
823 entries_before + 1,
824 "extend must persist exactly one (deepest) entry"
825 );
826
827 let deepest = find_special_token_boundaries(&turns[1], SPECIALS)
830 .into_iter()
831 .rev()
832 .find(|&b| b > prefix_len)
833 .expect("a deeper boundary must exist in the appended turn");
834 assert_eq!(
835 deepest_boundary, deepest,
836 "longest_prefix_match must return the deepest boundary used by extend"
837 );
838
839 let (saved_tokens, saved_offset, _deepest) = cache
842 .longest_prefix_match(&turns[1])
843 .expect("hit after extend");
844 assert_eq!(
845 saved_offset, deepest,
846 "lookup must now hit at the just-saved deepest boundary"
847 );
848 let expected = tok.encode(&turns[1][..deepest]).unwrap();
849 assert_eq!(
850 &*saved_tokens,
851 expected.token_ids(),
852 "persisted entry tokens must equal the uncached encode of the cached prefix"
853 );
854 }
855
856 #[test]
857 fn boundaries_detected_for_multibyte_deepseek_tool_tokens() {
858 let specials = &["<|tool▁calls▁begin|>", "<|tool▁call▁end|>"];
863 let text = "<|tool▁calls▁begin|>payload<|tool▁call▁end|>tail";
864 let bounds = find_special_token_boundaries(text, specials);
865
866 let after_begin = "<|tool▁calls▁begin|>".len();
867 let after_end = text.find("<|tool▁call▁end|>").unwrap() + "<|tool▁call▁end|>".len();
868 assert_eq!(bounds, vec![after_begin, after_end]);
869 for &b in &bounds {
870 assert!(
871 text.is_char_boundary(b),
872 "boundary {b} is not a char boundary"
873 );
874 let _ = &text[..b]; }
876 }
877
878 #[test]
879 fn populate_and_encode_matches_uncached_and_seeds_cache() {
880 let tok = load_tokenizer();
883 let cache = test_cache(8 * 1024 * 1024);
884 let input = "<s>system\nYou are helpful.</s><s>user\nHello there, friend.</s>";
885
886 let got = cache.populate_and_encode(input, tok.as_ref()).unwrap();
887 let plain = tok.encode(input).unwrap();
888 assert_eq!(
889 got,
890 plain.token_ids(),
891 "fused miss encode must equal uncached encode"
892 );
893
894 assert!(
896 !cache.is_empty(),
897 "miss path must populate boundary entries"
898 );
899 let (_t, offset, _d) = cache
900 .longest_prefix_match(input)
901 .expect("hit after populate");
902 assert!(offset > 0, "follow-up lookup should hit a cached boundary");
903 }
904
905 #[test]
906 fn populate_and_encode_handles_inputs_without_special_tokens() {
907 let tok = load_tokenizer();
910 let cache = test_cache(8 * 1024 * 1024);
911 let input = "plain text with no special tokens at all";
912
913 let got = cache.populate_and_encode(input, tok.as_ref()).unwrap();
914 let plain = tok.encode(input).unwrap();
915 assert_eq!(got, plain.token_ids());
916 assert!(cache.is_empty(), "nothing cacheable without boundaries");
917 }
918
919 #[test]
920 fn populate_and_encode_handles_trailing_special_token() {
921 let tok = load_tokenizer();
925 let cache = test_cache(8 * 1024 * 1024);
926 let input = "<s>system\nDone.</s>";
927
928 let got = cache.populate_and_encode(input, tok.as_ref()).unwrap();
929 let plain = tok.encode(input).unwrap();
930 assert_eq!(
931 got,
932 plain.token_ids(),
933 "tail-segment assembly must be exact"
934 );
935 }
936}