use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub struct NgramConfig {
pub min_ngram: usize,
pub max_ngram: usize,
pub k: usize,
pub max_model_len: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HistoryLookupConfig {
pub min_match: usize,
pub max_match: usize,
pub max_draft_tokens: usize,
pub max_model_len: usize,
}
impl HistoryLookupConfig {
pub fn default_for_decode(max_model_len: usize) -> Self {
Self {
min_match: 6,
max_match: 12,
max_draft_tokens: 3,
max_model_len,
}
}
}
impl NgramConfig {
pub fn default_for_decode(max_model_len: usize) -> Self {
Self {
min_ngram: 1,
max_ngram: 3,
k: 3,
max_model_len,
}
}
}
pub fn propose(tokens: &[u32], cfg: &NgramConfig) -> Vec<u32> {
let total = tokens.len();
if total < cfg.min_ngram {
return Vec::new();
}
let k_room = cfg.max_model_len.saturating_sub(total);
let k_capped = cfg.k.min(k_room);
if k_capped == 0 {
return Vec::new();
}
if cfg.max_ngram == 0 || cfg.min_ngram > cfg.max_ngram {
return Vec::new();
}
let rev = |i: usize| -> u32 { tokens[total - 1 - i] };
let lps_len = cfg.max_ngram;
let mut lps = vec![0u32; lps_len];
let mut longest_ngram: usize = 0;
let mut position: usize = 0;
let mut prev_lps: usize = 0;
let mut i: usize = 1;
while i < total {
if rev(prev_lps) == rev(i) {
prev_lps += 1;
if prev_lps >= longest_ngram {
longest_ngram = prev_lps;
position = i;
}
if i < lps_len {
lps[i] = prev_lps as u32;
}
if prev_lps == cfg.max_ngram {
prev_lps = lps[cfg.max_ngram - 1] as usize;
}
i += 1;
} else if prev_lps != 0 {
prev_lps = lps[prev_lps - 1] as usize;
} else {
i += 1;
}
}
if longest_ngram < cfg.min_ngram {
return Vec::new();
}
let start = total - 1 - position + longest_ngram;
let drafts_room = total.saturating_sub(start);
let n = k_capped.min(drafts_room);
if n == 0 {
return Vec::new();
}
tokens[start..start + n].to_vec()
}
pub fn propose_recent(tokens: &[u32], cfg: &HistoryLookupConfig) -> Vec<u32> {
let total = tokens.len();
if cfg.min_match == 0
|| cfg.max_match < cfg.min_match
|| cfg.max_draft_tokens == 0
|| total < cfg.min_match
{
return Vec::new();
}
let draft_room = cfg
.max_draft_tokens
.min(cfg.max_model_len.saturating_sub(total));
if draft_room == 0 {
return Vec::new();
}
let max_match = cfg.max_match.min(total);
for match_len in (cfg.min_match..=max_match).rev() {
let suffix_start = total - match_len;
if suffix_start == 0 {
continue;
}
let suffix = &tokens[suffix_start..];
for start in (0..suffix_start).rev() {
let end = start + match_len;
if end > total || &tokens[start..end] != suffix {
continue;
}
let available = total.saturating_sub(end);
let n = draft_room.min(available);
if n > 0 {
return tokens[end..end + n].to_vec();
}
}
}
Vec::new()
}
#[derive(Debug, Clone)]
pub struct HistoryLookupIndex {
cfg: HistoryLookupConfig,
tokens: Vec<u32>,
positions: HashMap<u32, Vec<usize>>,
}
impl HistoryLookupIndex {
pub fn new(cfg: HistoryLookupConfig) -> Self {
Self {
cfg,
tokens: Vec::new(),
positions: HashMap::new(),
}
}
pub fn reset(&mut self, tokens: &[u32]) {
self.tokens.clear();
self.positions.clear();
self.tokens.reserve(tokens.len());
for &token in tokens {
self.push_verified(token);
}
}
pub fn extend_verified(&mut self, tokens: &[u32]) {
self.tokens.reserve(tokens.len());
for &token in tokens {
self.push_verified(token);
}
}
pub fn verified_len(&self) -> usize {
self.tokens.len()
}
pub fn propose(&self) -> Vec<u32> {
let total = self.tokens.len();
if self.cfg.min_match == 0
|| self.cfg.max_match < self.cfg.min_match
|| self.cfg.max_draft_tokens == 0
|| total < self.cfg.min_match
{
return Vec::new();
}
let draft_room = self
.cfg
.max_draft_tokens
.min(self.cfg.max_model_len.saturating_sub(total));
if draft_room == 0 {
return Vec::new();
}
let Some(candidate_ends) = self.positions.get(&self.tokens[total - 1]) else {
return Vec::new();
};
let max_match = self.cfg.max_match.min(total);
for match_len in (self.cfg.min_match..=max_match).rev() {
let suffix_start = total - match_len;
if suffix_start == 0 {
continue;
}
let suffix = &self.tokens[suffix_start..];
for &end in candidate_ends.iter().rev() {
if end >= total - 1 || end + 1 < match_len {
continue;
}
let start = end + 1 - match_len;
if start >= suffix_start || &self.tokens[start..=end] != suffix {
continue;
}
let continuation = end + 1;
let n = draft_room.min(total - continuation);
if n > 0 {
return self.tokens[continuation..continuation + n].to_vec();
}
}
}
Vec::new()
}
fn push_verified(&mut self, token: u32) {
let position = self.tokens.len();
self.tokens.push(token);
self.positions.entry(token).or_default().push(position);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(min_n: usize, max_n: usize, k: usize) -> NgramConfig {
NgramConfig {
min_ngram: min_n,
max_ngram: max_n,
k,
max_model_len: 4096,
}
}
#[test]
fn propose_empty_when_below_min_ngram() {
assert!(propose(&[], &cfg(1, 3, 3)).is_empty());
assert!(propose(&[7], &cfg(2, 3, 3)).is_empty());
}
#[test]
fn propose_empty_when_no_match() {
let drafts = propose(&[1, 2, 3, 4, 5, 6], &cfg(2, 3, 3));
assert!(drafts.is_empty(), "expected no drafts, got {:?}", drafts);
}
#[test]
fn propose_basic_repetition() {
let tokens = vec![10u32, 20, 30, 99, 88, 10, 20, 30];
let drafts = propose(&tokens, &cfg(1, 3, 3));
assert_eq!(drafts, vec![99, 88, 10]);
}
#[test]
fn propose_respects_k_truncation() {
let tokens = vec![10u32, 20, 30, 99, 88, 10, 20, 30];
let drafts = propose(&tokens, &cfg(1, 3, 2));
assert_eq!(drafts, vec![99, 88]);
}
#[test]
fn propose_respects_max_ngram_cap() {
let tokens = vec![1u32, 2, 3, 4, 5, 99, 1, 2, 3, 4, 5];
let drafts = propose(&tokens, &cfg(2, 2, 3));
assert_eq!(drafts, vec![99, 1, 2]);
}
#[test]
fn propose_picks_earliest_occurrence_on_tie() {
let tokens = vec![10u32, 20, 100, 10, 20, 200, 10, 20];
let drafts = propose(&tokens, &cfg(1, 3, 3));
assert_eq!(drafts, vec![100, 10, 20]);
}
#[test]
fn propose_caps_k_at_max_model_len() {
let cfg = NgramConfig {
min_ngram: 1,
max_ngram: 3,
k: 5,
max_model_len: 10, };
let tokens = vec![10u32, 20, 30, 99, 88, 10, 20, 30];
let drafts = propose(&tokens, &cfg);
assert_eq!(drafts.len(), 2, "expected k clamped to max_model_len - len");
assert_eq!(drafts, vec![99, 88]);
}
#[test]
fn propose_handles_longest_match_at_seq_end() {
let tokens = vec![1u32, 2, 3, 1, 2, 3];
let drafts = propose(&tokens, &cfg(1, 3, 3));
assert_eq!(drafts, vec![1, 2, 3]);
}
#[test]
fn propose_zero_max_ngram_returns_empty() {
let bad_cfg = NgramConfig {
min_ngram: 0,
max_ngram: 0,
k: 3,
max_model_len: 4096,
};
assert!(propose(&[1, 2, 3], &bad_cfg).is_empty());
}
#[test]
fn propose_k_zero_returns_empty() {
let bad_cfg = NgramConfig {
min_ngram: 1,
max_ngram: 3,
k: 0,
max_model_len: 4096,
};
assert!(propose(&[1, 2, 3], &bad_cfg).is_empty());
}
#[test]
fn default_config_is_reasonable() {
let cfg = NgramConfig::default_for_decode(4096);
assert_eq!(cfg.k, 3);
assert_eq!(cfg.min_ngram, 1);
assert_eq!(cfg.max_ngram, 3);
assert_eq!(cfg.max_model_len, 4096);
}
fn rand_tokens(seed: u64, n: usize, vocab: u32) -> Vec<u32> {
let mut state = seed;
(0..n)
.map(|_| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 33) as u32) % vocab
})
.collect()
}
#[test]
#[ignore]
fn bench_ngram_proposer_at_realistic_decode_lengths() {
use std::time::Instant;
let cfg = NgramConfig {
min_ngram: 1,
max_ngram: 3,
k: 3,
max_model_len: 16_384,
};
let lengths = [128usize, 512, 1024, 2048, 4096, 8192];
for &n in &lengths {
let tokens = rand_tokens(0xCAFE_BEEF, n, 256);
for _ in 0..100 {
let _ = propose(&tokens, &cfg);
}
let mut samples: Vec<u128> = Vec::with_capacity(1000);
for _ in 0..1000 {
let t0 = Instant::now();
let _ = propose(&tokens, &cfg);
samples.push(t0.elapsed().as_nanos());
}
samples.sort();
let p50 = samples[500];
let p99 = samples[990];
eprintln!(
"[BENCH iter-115] propose len={:5} p50={:6} ns p99={:6} ns",
n, p50, p99
);
assert!(
(p50 as usize) < 100_000,
"propose at len={n} took {p50} ns p50 — too slow for hot path (target <100 µs)"
);
}
}
#[test]
fn history_lookup_prefers_longest_suffix() {
let tokens = vec![1, 2, 3, 4, 5, 6, 70, 71, 9, 5, 6, 80, 1, 2, 3, 4, 5, 6];
let cfg = HistoryLookupConfig {
min_match: 2,
max_match: 6,
max_draft_tokens: 2,
max_model_len: 4096,
};
assert_eq!(propose_recent(&tokens, &cfg), vec![70, 71]);
}
#[test]
fn history_lookup_prefers_most_recent_occurrence_on_tie() {
let key = [10, 11, 12, 13, 14, 15];
let mut tokens = Vec::new();
tokens.extend_from_slice(&key);
tokens.extend_from_slice(&[100, 101]);
tokens.extend_from_slice(&key);
tokens.extend_from_slice(&[200, 201]);
tokens.extend_from_slice(&key);
let cfg = HistoryLookupConfig {
max_draft_tokens: 2,
..HistoryLookupConfig::default_for_decode(4096)
};
assert_eq!(propose_recent(&tokens, &cfg), vec![200, 201]);
}
#[test]
fn history_lookup_caps_draft_at_context_room() {
let tokens = vec![1, 2, 3, 4, 5, 6, 90, 91, 92, 1, 2, 3, 4, 5, 6];
let cfg = HistoryLookupConfig {
min_match: 6,
max_match: 12,
max_draft_tokens: 5,
max_model_len: tokens.len() + 2,
};
assert_eq!(propose_recent(&tokens, &cfg), vec![90, 91]);
}
#[test]
fn history_lookup_rejects_invalid_or_unmatched_config() {
let tokens = [1, 2, 3, 4, 5, 6];
assert!(propose_recent(
&tokens,
&HistoryLookupConfig {
min_match: 0,
max_match: 12,
max_draft_tokens: 5,
max_model_len: 4096,
}
)
.is_empty());
assert!(propose_recent(&tokens, &HistoryLookupConfig::default_for_decode(4096)).is_empty());
}
#[test]
fn history_lookup_index_matches_scan_and_updates_only_on_commit() {
let cfg = HistoryLookupConfig::default_for_decode(4096);
let initial = [1, 2, 3, 4, 5, 6, 90, 91, 1, 2, 3, 4, 5, 6];
let mut index = HistoryLookupIndex::new(cfg);
index.reset(&initial);
assert_eq!(index.propose(), propose_recent(&initial, &cfg));
assert_eq!(index.propose(), vec![90, 91, 1]);
let unverified = [90, 91];
assert_eq!(index.verified_len(), initial.len());
assert_eq!(index.propose(), vec![90, 91, 1]);
index.extend_verified(&unverified[..1]);
let mut committed = initial.to_vec();
committed.push(90);
assert_eq!(index.propose(), propose_recent(&committed, &cfg));
assert_eq!(index.verified_len(), committed.len());
}
#[test]
fn history_lookup_index_matches_scan_across_random_prefixes() {
let cfg = HistoryLookupConfig {
min_match: 2,
max_match: 8,
max_draft_tokens: 5,
max_model_len: 4096,
};
let mut tokens = rand_tokens(0xA11C_E5ED, 300, 32);
let repeated = tokens[40..70].to_vec();
tokens.extend_from_slice(&repeated);
let mut index = HistoryLookupIndex::new(cfg);
index.reset(&tokens[..16]);
for &token in &tokens[16..] {
assert_eq!(index.propose(), propose_recent(&index.tokens, &cfg));
index.extend_verified(&[token]);
}
assert_eq!(index.propose(), propose_recent(&tokens, &cfg));
}
#[test]
#[ignore]
fn bench_history_lookup_random_miss_and_recent_hit() {
use std::time::Instant;
for &n in &[8_192usize, 100_000] {
let mut random = rand_tokens(0x1385_9420, n, 248_064);
let cfg = HistoryLookupConfig::default_for_decode(n + 256);
let hit_tail = random[n - 64..n].to_vec();
let mut miss_ns = Vec::with_capacity(200);
for _ in 0..200 {
let start = Instant::now();
std::hint::black_box(propose_recent(std::hint::black_box(&random), &cfg));
miss_ns.push(start.elapsed().as_nanos());
}
random.extend_from_slice(&hit_tail[..12]);
let mut hit_ns = Vec::with_capacity(200);
for _ in 0..200 {
let start = Instant::now();
std::hint::black_box(propose_recent(std::hint::black_box(&random), &cfg));
hit_ns.push(start.elapsed().as_nanos());
}
miss_ns.sort_unstable();
hit_ns.sort_unstable();
eprintln!(
"history_lookup len={n} miss_p50={}ns miss_p99={}ns hit_p50={}ns hit_p99={}ns",
miss_ns[100], miss_ns[198], hit_ns[100], hit_ns[198]
);
}
}
#[test]
#[ignore]
fn bench_history_lookup_index_random_miss_and_recent_hit() {
use std::time::Instant;
for &n in &[8_192usize, 100_000] {
let random = rand_tokens(0x1385_9420, n, 248_064);
let cfg = HistoryLookupConfig::default_for_decode(n + 256);
let mut index = HistoryLookupIndex::new(cfg);
index.reset(&random);
let mut miss_ns = Vec::with_capacity(1_000);
for _ in 0..1_000 {
let start = Instant::now();
std::hint::black_box(index.propose());
miss_ns.push(start.elapsed().as_nanos());
}
index.extend_verified(&random[n - 64..n - 52]);
let mut hit_ns = Vec::with_capacity(1_000);
for _ in 0..1_000 {
let start = Instant::now();
std::hint::black_box(index.propose());
hit_ns.push(start.elapsed().as_nanos());
}
miss_ns.sort_unstable();
hit_ns.sort_unstable();
eprintln!(
"history_lookup_index len={n} miss_p50={}ns miss_p99={}ns hit_p50={}ns hit_p99={}ns",
miss_ns[500], miss_ns[990], hit_ns[500], hit_ns[990]
);
}
}
}