use fuel::Result;
#[derive(Debug, Clone)]
pub struct SpeculativeConfig {
pub draft_len: usize,
pub min_acceptance_rate: f32,
pub stats_window: usize,
}
impl SpeculativeConfig {
pub fn new(draft_len: usize) -> Self {
Self {
draft_len: draft_len.max(1),
min_acceptance_rate: 0.0,
stats_window: 100,
}
}
pub fn with_min_acceptance_rate(mut self, rate: f32) -> Self {
self.min_acceptance_rate = rate.clamp(0.0, 1.0);
self
}
pub fn with_stats_window(mut self, window: usize) -> Self {
self.stats_window = window.max(1);
self
}
}
#[derive(Debug, Clone)]
pub struct SpeculativeStats {
pub total_rounds: u64,
pub total_drafted: u64,
pub total_accepted: u64,
recent_rates: Vec<f32>,
window_idx: usize,
}
impl SpeculativeStats {
pub fn new() -> Self {
Self {
total_rounds: 0,
total_drafted: 0,
total_accepted: 0,
recent_rates: Vec::new(),
window_idx: 0,
}
}
pub fn record(&mut self, drafted: usize, accepted: usize, window_size: usize) {
self.total_rounds += 1;
self.total_drafted += drafted as u64;
self.total_accepted += accepted as u64;
let rate = if drafted > 0 {
accepted as f32 / drafted as f32
} else {
1.0
};
if self.recent_rates.len() < window_size {
self.recent_rates.push(rate);
} else {
let idx = self.window_idx % window_size;
self.recent_rates[idx] = rate;
}
self.window_idx += 1;
}
pub fn acceptance_rate(&self) -> f32 {
if self.total_drafted == 0 {
1.0
} else {
self.total_accepted as f32 / self.total_drafted as f32
}
}
pub fn recent_acceptance_rate(&self) -> f32 {
if self.recent_rates.is_empty() {
1.0
} else {
self.recent_rates.iter().sum::<f32>() / self.recent_rates.len() as f32
}
}
pub fn should_fallback(&self, config: &SpeculativeConfig) -> bool {
config.min_acceptance_rate > 0.0
&& self.total_rounds >= config.stats_window as u64
&& self.recent_acceptance_rate() < config.min_acceptance_rate
}
}
impl Default for SpeculativeStats {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct VerifyResult {
pub accepted_tokens: Vec<u32>,
pub next_token: u32,
pub num_accepted: usize,
}
pub fn verify_draft(
draft_tokens: &[u32],
draft_logprobs: &[Vec<f32>],
target_logprobs: &[Vec<f32>],
config: &SpeculativeConfig,
stats: &mut SpeculativeStats,
) -> Result<VerifyResult> {
let k = draft_tokens.len().min(config.draft_len);
if k == 0 {
let target_probs = softmax_1d(row(target_logprobs, 0, "target_logprobs")?);
let next_token = sample_from_probs(&target_probs)?;
stats.record(0, 0, config.stats_window);
return Ok(VerifyResult {
accepted_tokens: Vec::new(),
next_token,
num_accepted: 0,
});
}
let mut accepted = Vec::with_capacity(k);
for (i, &token) in draft_tokens.iter().enumerate().take(k) {
let draft_probs = softmax_1d(row(draft_logprobs, i, "draft_logprobs")?);
let target_probs = softmax_1d(row(target_logprobs, i, "target_logprobs")?);
let draft_p = prob_of_token(&draft_probs, token)?;
let target_p = prob_of_token(&target_probs, token)?;
let accept_prob = if draft_p > 0.0 {
(target_p / draft_p).min(1.0)
} else if target_p > 0.0 {
1.0 } else {
0.0 };
let r = pseudo_uniform(i as u64, token as u64);
if r < accept_prob {
accepted.push(token);
} else {
let next_token = resample_adjusted(&target_probs, &draft_probs)?;
stats.record(k, accepted.len(), config.stats_window);
return Ok(VerifyResult {
accepted_tokens: accepted,
next_token,
num_accepted: i,
});
}
}
let bonus_probs = softmax_1d(row(target_logprobs, k, "target_logprobs")?);
let next_token = sample_from_probs(&bonus_probs)?;
stats.record(k, k, config.stats_window);
Ok(VerifyResult {
accepted_tokens: accepted,
next_token,
num_accepted: k,
})
}
fn row<'a>(rows: &'a [Vec<f32>], i: usize, name: &str) -> Result<&'a [f32]> {
rows.get(i).map(Vec::as_slice).ok_or_else(|| {
fuel::Error::Msg(format!(
"{name}: need row {i} but only {} row(s) supplied",
rows.len(),
))
})
}
fn softmax_1d(logits: &[f32]) -> Vec<f32> {
let max_val = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
if !max_val.is_finite() {
return vec![0.0; logits.len()];
}
let mut probs: Vec<f32> = logits.iter().map(|&l| (l - max_val).exp()).collect();
let sum: f32 = probs.iter().sum();
if sum > 0.0 {
for p in &mut probs {
*p /= sum;
}
}
probs
}
fn prob_of_token(probs: &[f32], token: u32) -> Result<f32> {
Ok(probs.get(token as usize).copied().unwrap_or(0.0))
}
fn resample_adjusted(target_probs: &[f32], draft_probs: &[f32]) -> Result<u32> {
let adjusted: Vec<f32> = target_probs
.iter()
.zip(draft_probs.iter())
.map(|(&t, &d)| (t - d).max(0.0))
.collect();
let sum: f32 = adjusted.iter().sum();
if sum <= 0.0 {
return sample_from_probs(target_probs);
}
let normalized: Vec<f32> = adjusted.iter().map(|&p| p / sum).collect();
sample_from_probs(&normalized)
}
fn sample_from_probs(probs: &[f32]) -> Result<u32> {
let mut best_idx = 0u32;
let mut best_p = f32::NEG_INFINITY;
for (i, &p) in probs.iter().enumerate() {
if p > best_p {
best_p = p;
best_idx = i as u32;
}
}
Ok(best_idx)
}
fn pseudo_uniform(position: u64, token: u64) -> f32 {
let mut x = position
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add(token);
x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
(x >> 40) as f32 / (1u64 << 24) as f32
}
#[cfg(test)]
mod tests {
use super::*;
use fuel::Device;
fn uniform_logprobs(k: usize, vocab: usize) -> Vec<Vec<f32>> {
vec![vec![0.0_f32; vocab]; k]
}
#[test]
fn all_accepted_when_distributions_match() -> Result<()> {
let _device = Device::cpu();
let vocab = 50;
let k = 3;
let draft_tokens = vec![5u32, 10, 15];
let logprobs = uniform_logprobs(k, vocab);
let target_logprobs = uniform_logprobs(k + 1, vocab);
let config = SpeculativeConfig::new(k);
let mut stats = SpeculativeStats::new();
let result = verify_draft(
&draft_tokens,
&logprobs,
&target_logprobs,
&config,
&mut stats,
)?;
assert_eq!(result.num_accepted, 3);
assert_eq!(result.accepted_tokens, vec![5, 10, 15]);
assert_eq!(stats.total_drafted, 3);
assert_eq!(stats.total_accepted, 3);
Ok(())
}
#[test]
fn stats_tracking() {
let mut stats = SpeculativeStats::new();
stats.record(5, 3, 10);
stats.record(5, 5, 10);
assert_eq!(stats.total_rounds, 2);
assert_eq!(stats.total_drafted, 10);
assert_eq!(stats.total_accepted, 8);
assert!((stats.acceptance_rate() - 0.8).abs() < 1e-6);
}
#[test]
fn stats_rolling_window() {
let mut stats = SpeculativeStats::new();
stats.record(10, 10, 3); stats.record(10, 5, 3); stats.record(10, 0, 3);
assert_eq!(stats.recent_rates.len(), 3);
let recent = stats.recent_acceptance_rate();
assert!((recent - 0.5).abs() < 1e-6);
stats.record(10, 8, 3); let recent = stats.recent_acceptance_rate();
let expected = (0.8 + 0.5 + 0.0) / 3.0;
assert!((recent - expected).abs() < 1e-6);
}
#[test]
fn fallback_detection() {
let config = SpeculativeConfig::new(5).with_min_acceptance_rate(0.5);
let mut stats = SpeculativeStats::new();
assert!(!stats.should_fallback(&config));
for _ in 0..100 {
stats.record(5, 1, config.stats_window);
}
assert!(stats.should_fallback(&config));
}
#[test]
fn empty_draft_returns_bonus_token() -> Result<()> {
let _device = Device::cpu();
let vocab = 50;
let draft_tokens: Vec<u32> = vec![];
let draft_logprobs: Vec<Vec<f32>> = Vec::new();
let target_logprobs = vec![vec![0.0_f32; vocab]];
let config = SpeculativeConfig::new(3);
let mut stats = SpeculativeStats::new();
let result = verify_draft(
&draft_tokens,
&draft_logprobs,
&target_logprobs,
&config,
&mut stats,
)?;
assert_eq!(result.num_accepted, 0);
assert!(result.accepted_tokens.is_empty());
Ok(())
}
#[test]
fn rejection_with_divergent_distributions() -> Result<()> {
let _device = Device::cpu();
let vocab = 10;
let k = 3;
let mut draft_data = vec![0.0f32; k * vocab];
for i in 0..k {
draft_data[i * vocab] = 10.0; }
let draft_logprobs: Vec<Vec<f32>> = draft_data.chunks(vocab).map(<[f32]>::to_vec).collect();
let mut target_data = vec![0.0f32; (k + 1) * vocab];
for i in 0..=k {
target_data[i * vocab + 5] = 10.0; }
let target_logprobs: Vec<Vec<f32>> =
target_data.chunks(vocab).map(<[f32]>::to_vec).collect();
let draft_tokens = vec![0u32, 0, 0];
let config = SpeculativeConfig::new(k);
let mut stats = SpeculativeStats::new();
let result = verify_draft(
&draft_tokens,
&draft_logprobs,
&target_logprobs,
&config,
&mut stats,
)?;
assert!(result.num_accepted < k);
assert_eq!(result.next_token, 5);
Ok(())
}
#[test]
fn verify_result_invariants() -> Result<()> {
let _device = Device::cpu();
let vocab = 20;
let k = 5;
let draft_tokens: Vec<u32> = (0..k as u32).collect();
let draft_logprobs = uniform_logprobs(k, vocab);
let target_logprobs = uniform_logprobs(k + 1, vocab);
let config = SpeculativeConfig::new(k);
let mut stats = SpeculativeStats::new();
let result = verify_draft(
&draft_tokens,
&draft_logprobs,
&target_logprobs,
&config,
&mut stats,
)?;
assert_eq!(result.accepted_tokens.len(), result.num_accepted);
assert!(result.num_accepted <= k);
for (i, &tok) in result.accepted_tokens.iter().enumerate() {
assert_eq!(tok, draft_tokens[i]);
}
Ok(())
}
#[test]
fn pseudo_uniform_in_range() {
for pos in 0..1000 {
for tok in [0u64, 1, 42, 1000, u64::MAX] {
let v = pseudo_uniform(pos, tok);
assert!((0.0..1.0).contains(&v), "pos={pos}, tok={tok}, v={v}");
}
}
}
#[test]
fn config_builder() {
let config = SpeculativeConfig::new(5)
.with_min_acceptance_rate(0.3)
.with_stats_window(50);
assert_eq!(config.draft_len, 5);
assert!((config.min_acceptance_rate - 0.3).abs() < 1e-6);
assert_eq!(config.stats_window, 50);
}
}