use std::sync::Arc;
use cudarc::driver::CudaDevice;
use super::compute::ComputeEngine;
use super::generate::SamplingParams;
use super::kv_cache::KvCache;
use super::weight_store::WeightStore;
use super::InferenceError;
#[derive(Debug, Clone)]
pub struct SpeculativeConfig {
pub num_candidates: usize,
pub temperature_adjustment: f32,
pub draft_top_p: f32,
pub max_rejections: usize,
}
impl Default for SpeculativeConfig {
fn default() -> Self {
Self {
num_candidates: 5,
temperature_adjustment: 1.0,
draft_top_p: 0.9,
max_rejections: 3,
}
}
}
#[derive(Debug, Clone)]
pub struct TokenCandidate {
pub token: u32,
pub draft_prob: f32,
pub target_prob: f32,
}
#[derive(Debug)]
pub struct VerificationResult {
pub accepted: Vec<u32>,
pub sampled: u32,
pub total_tokens: usize,
pub acceptance_rate: f32,
}
pub struct SpeculativeDecoder {
draft_engine: ComputeEngine,
draft_weights: Arc<WeightStore>,
draft_kv: KvCache,
target_engine: ComputeEngine,
target_weights: Arc<WeightStore>,
target_kv: KvCache,
config: SpeculativeConfig,
#[allow(dead_code)]
device: Arc<CudaDevice>,
rejection_count: usize,
total_accepted: usize,
total_proposed: usize,
}
impl SpeculativeDecoder {
pub fn new(
draft_weights: Arc<WeightStore>,
target_weights: Arc<WeightStore>,
max_seq_len: usize,
device: Arc<CudaDevice>,
config: SpeculativeConfig,
) -> Result<Self, InferenceError> {
let draft_config = draft_weights.config.clone();
let target_config = target_weights.config.clone();
let draft_engine =
ComputeEngine::new(draft_config.clone(), max_seq_len, Arc::clone(&device))?;
let target_engine =
ComputeEngine::new(target_config.clone(), max_seq_len, Arc::clone(&device))?;
let draft_kv = KvCache::new(&draft_config, max_seq_len, Arc::clone(&device))?;
let target_kv = KvCache::new(&target_config, max_seq_len, Arc::clone(&device))?;
Ok(Self {
draft_engine,
draft_weights,
draft_kv,
target_engine,
target_weights,
target_kv,
config,
device,
rejection_count: 0,
total_accepted: 0,
total_proposed: 0,
})
}
pub fn step(
&mut self,
input_ids: &[u32],
position: usize,
params: &SamplingParams,
) -> Result<VerificationResult, InferenceError> {
let candidates = self.generate_draft_candidates(input_ids, position, params)?;
let num_candidates = candidates.len();
if num_candidates == 0 {
let token = self.generate_single_token(input_ids, position)?;
return Ok(VerificationResult {
accepted: Vec::new(),
sampled: token,
total_tokens: 1,
acceptance_rate: 0.0,
});
}
let result = self.verify_candidates(input_ids, position, &candidates, params)?;
self.total_proposed += num_candidates;
self.total_accepted += result.accepted.len();
if result.accepted.is_empty() {
self.rejection_count += 1;
} else {
self.rejection_count = 0;
}
Ok(result)
}
fn generate_draft_candidates(
&mut self,
input_ids: &[u32],
position: usize,
params: &SamplingParams,
) -> Result<Vec<TokenCandidate>, InferenceError> {
let mut candidates = Vec::with_capacity(self.config.num_candidates);
let mut draft_ids = input_ids.to_vec();
let mut pos = position;
let mut draft_params = params.clone();
draft_params.top_p = self.config.draft_top_p;
for _ in 0..self.config.num_candidates {
let logits = self
.draft_engine
.forward(&draft_ids, &self.draft_weights, pos)?;
let (token, prob) = self.sample_from_logits(&logits, &draft_params)?;
candidates.push(TokenCandidate {
token,
draft_prob: prob,
target_prob: 0.0, });
draft_ids.push(token);
pos += 1;
}
Ok(candidates)
}
fn verify_candidates(
&mut self,
input_ids: &[u32],
position: usize,
candidates: &[TokenCandidate],
params: &SamplingParams,
) -> Result<VerificationResult, InferenceError> {
let n = candidates.len();
let mut verify_ids = input_ids.to_vec();
for c in candidates {
verify_ids.push(c.token);
}
let logits = self
.target_engine
.forward(&verify_ids, &self.target_weights, position)?;
let mut accepted = Vec::new();
for (i, candidate) in candidates.iter().enumerate() {
let target_prob = self.get_token_prob_from_logits(&logits, candidate.token, i)?;
let accept_prob = (target_prob / candidate.draft_prob.max(1e-10)).min(1.0);
let rand: f32 = fastrand::f32();
if rand < accept_prob {
accepted.push(candidate.token);
} else {
break; }
}
let sampled = self.sample_final_token(&logits, params)?;
let num_accepted = accepted.len();
let acceptance_rate = if n > 0 {
num_accepted as f32 / n as f32
} else {
0.0
};
Ok(VerificationResult {
accepted,
sampled,
total_tokens: num_accepted + 1,
acceptance_rate,
})
}
fn generate_single_token(
&mut self,
input_ids: &[u32],
position: usize,
) -> Result<u32, InferenceError> {
let logits = self
.target_engine
.forward(input_ids, &self.target_weights, position)?;
let cpu_logits = self.logits_to_cpu(&logits)?;
let token = cpu_logits
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(idx, _)| idx as u32)
.unwrap_or(0);
Ok(token)
}
fn sample_from_logits(
&self,
logits: &super::tensor::GpuTensor,
params: &SamplingParams,
) -> Result<(u32, f32), InferenceError> {
let cpu_logits = self.logits_to_cpu(logits)?;
let scaled: Vec<f32> = cpu_logits.iter().map(|&x| x / params.temperature).collect();
let max = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exp_sum: f32 = scaled.iter().map(|&x| (x - max).exp()).sum();
let probs: Vec<f32> = scaled.iter().map(|&x| (x - max).exp() / exp_sum).collect();
let mut sorted_indices: Vec<usize> = (0..probs.len()).collect();
sorted_indices.sort_by(|&a, &b| probs[b].partial_cmp(&probs[a]).unwrap());
let mut cumsum = 0.0;
let mut nucleus = Vec::new();
for &idx in &sorted_indices {
cumsum += probs[idx];
nucleus.push((idx, probs[idx]));
if cumsum >= params.top_p {
break;
}
}
let nucleus_sum: f32 = nucleus.iter().map(|(_, p)| p).sum();
let rand: f32 = fastrand::f32();
let mut accum = 0.0;
for (idx, prob) in nucleus {
accum += prob / nucleus_sum;
if rand < accum {
return Ok((idx as u32, prob));
}
}
let (idx, &prob) = probs
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
Ok((idx as u32, prob))
}
fn get_token_prob_from_logits(
&self,
logits: &super::tensor::GpuTensor,
token: u32,
_position: usize,
) -> Result<f32, InferenceError> {
let cpu_logits = self.logits_to_cpu(logits)?;
let vocab_size = cpu_logits.len();
if (token as usize) >= vocab_size {
return Ok(0.0);
}
let max = cpu_logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exp_sum: f32 = cpu_logits.iter().map(|&x| (x - max).exp()).sum();
let prob = (cpu_logits[token as usize] - max).exp() / exp_sum;
Ok(prob)
}
fn sample_final_token(
&self,
logits: &super::tensor::GpuTensor,
params: &SamplingParams,
) -> Result<u32, InferenceError> {
let (token, _) = self.sample_from_logits(logits, params)?;
Ok(token)
}
fn logits_to_cpu(&self, logits: &super::tensor::GpuTensor) -> Result<Vec<f32>, InferenceError> {
let bytes = logits.to_host()?;
let f16_values: Vec<half::f16> = bytes
.chunks_exact(2)
.map(|chunk| half::f16::from_le_bytes([chunk[0], chunk[1]]))
.collect();
Ok(f16_values.iter().map(|x| x.to_f32()).collect())
}
pub fn acceptance_rate(&self) -> f32 {
if self.total_proposed == 0 {
0.0
} else {
self.total_accepted as f32 / self.total_proposed as f32
}
}
pub fn estimated_speedup(&self) -> f32 {
let alpha = self.acceptance_rate();
let k = self.config.num_candidates as f32;
if alpha < 0.01 {
1.0
} else {
(1.0 - alpha.powf(k + 1.0)) / (1.0 - alpha)
}
}
pub fn should_fallback(&self) -> bool {
self.rejection_count >= self.config.max_rejections
}
pub fn reset(&mut self) {
self.draft_kv.reset();
self.target_kv.reset();
self.rejection_count = 0;
}
pub fn reset_stats(&mut self) {
self.total_accepted = 0;
self.total_proposed = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_speculative_config_default() {
let config = SpeculativeConfig::default();
assert_eq!(config.num_candidates, 5);
assert_eq!(config.draft_top_p, 0.9);
}
#[test]
fn test_acceptance_rate_calculation() {
let total_proposed = 0;
let total_accepted = 0;
let rate = if total_proposed == 0 {
0.0
} else {
total_accepted as f32 / total_proposed as f32
};
assert_eq!(rate, 0.0);
let total_proposed = 100;
let total_accepted = 70;
let rate = total_accepted as f32 / total_proposed as f32;
assert!((rate - 0.7).abs() < 0.01);
}
#[test]
fn test_speedup_formula() {
let alpha = 0.7_f32;
let k = 5.0_f32;
let expected = (1.0 - alpha.powf(k + 1.0)) / (1.0 - alpha);
assert!(expected > 2.0 && expected < 3.5);
let alpha = 0.9_f32;
let expected = (1.0 - alpha.powf(k + 1.0)) / (1.0 - alpha);
assert!(expected > 4.0 && expected < 5.5);
}
}