use std::sync::Arc;
use std::time::Instant;
use candle_core::{DType, Device, IndexOp, Tensor};
use parking_lot::Mutex;
#[derive(Debug, Clone)]
pub struct Speculative405BConfig {
pub num_draft_tokens: usize,
pub acceptance_threshold: f32,
pub draft_temperature: f32,
pub target_temperature: f32,
pub greedy_draft: bool,
}
impl Default for Speculative405BConfig {
fn default() -> Self {
Self {
num_draft_tokens: 5,
acceptance_threshold: 0.1, draft_temperature: 0.7,
target_temperature: 0.7,
greedy_draft: true, }
}
}
impl Speculative405BConfig {
pub fn fast() -> Self {
Self {
num_draft_tokens: 8, acceptance_threshold: 0.05, greedy_draft: true,
..Default::default()
}
}
pub fn quality() -> Self {
Self {
num_draft_tokens: 4, acceptance_threshold: 0.2, greedy_draft: true,
..Default::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Speculative405BStats {
pub rounds: u64,
pub draft_tokens: u64,
pub accepted_tokens: u64,
pub rejected_tokens: u64,
pub target_forward_passes: u64,
pub draft_forward_passes: u64,
pub draft_time_ms: u64,
pub verify_time_ms: u64,
}
impl Speculative405BStats {
pub fn acceptance_rate(&self) -> f32 {
if self.draft_tokens == 0 {
0.0
} else {
self.accepted_tokens as f32 / self.draft_tokens as f32
}
}
pub fn tokens_per_round(&self) -> f32 {
if self.rounds == 0 {
0.0
} else {
(self.accepted_tokens + self.rounds) as f32 / self.rounds as f32
}
}
pub fn speedup(&self) -> f32 {
if self.target_forward_passes == 0 {
1.0
} else {
let total_tokens = self.accepted_tokens + self.rounds;
total_tokens as f32 / self.target_forward_passes as f32
}
}
}
pub trait DraftModel: Send {
fn forward(&mut self, input_ids: &Tensor, pos: usize) -> candle_core::Result<Tensor>;
fn clear_cache(&mut self);
fn device(&self) -> &Device;
fn dtype(&self) -> DType;
}
pub trait TargetModel: Send {
fn forward(&mut self, input_ids: &Tensor, pos: usize) -> anyhow::Result<Tensor>;
fn clear_cache(&mut self);
}
pub struct Speculative405B<D: DraftModel, T: TargetModel = crate::models::lazy_llama::LazyLlama> {
draft: Arc<Mutex<D>>,
target: Arc<Mutex<T>>,
config: Speculative405BConfig,
device: Device,
#[allow(dead_code)]
dtype: DType,
stats: Mutex<Speculative405BStats>,
}
impl<D: DraftModel, T: TargetModel> Speculative405B<D, T> {
pub fn new(draft: D, target: T, config: Speculative405BConfig) -> Self {
let device = draft.device().clone();
let dtype = draft.dtype();
Self {
draft: Arc::new(Mutex::new(draft)),
target: Arc::new(Mutex::new(target)),
config,
device,
dtype,
stats: Mutex::new(Speculative405BStats::default()),
}
}
pub fn stats(&self) -> Speculative405BStats {
self.stats.lock().clone()
}
pub fn reset_stats(&self) {
*self.stats.lock() = Speculative405BStats::default();
}
pub fn generate(
&self,
prompt_tokens: &[u32],
max_tokens: usize,
eos_token: u32,
) -> anyhow::Result<Vec<u32>> {
let mut generated = Vec::with_capacity(max_tokens);
let num_draft = self.config.num_draft_tokens;
{
self.draft.lock().clear_cache();
self.target.lock().clear_cache();
}
println!(" Prefilling draft model...");
let prefill_start = Instant::now();
{
let input = Tensor::new(prompt_tokens, &self.device)?.unsqueeze(0)?;
let mut draft = self.draft.lock();
let _ = draft.forward(&input, 0)?;
}
println!(" Draft prefill: {:?}", prefill_start.elapsed());
println!(" Prefilling 405B model...");
let prefill_start = Instant::now();
{
let input = Tensor::new(prompt_tokens, &self.device)?.unsqueeze(0)?;
let mut target = self.target.lock();
let _ = target.forward(&input, 0)?;
}
println!(" 405B prefill: {:?}", prefill_start.elapsed());
let mut current_pos = prompt_tokens.len();
while generated.len() < max_tokens {
self.stats.lock().rounds += 1;
let draft_start = Instant::now();
let draft_tokens =
self.generate_draft_tokens(prompt_tokens, &generated, num_draft, eos_token)?;
let draft_elapsed = draft_start.elapsed().as_millis() as u64;
{
let mut stats = self.stats.lock();
stats.draft_time_ms += draft_elapsed;
stats.draft_tokens += draft_tokens.len() as u64;
stats.draft_forward_passes += draft_tokens.len() as u64;
}
if draft_tokens.is_empty() {
break;
}
let verify_start = Instant::now();
let (accepted, next_token) =
self.verify_draft_tokens(prompt_tokens, &generated, &draft_tokens, current_pos)?;
let verify_elapsed = verify_start.elapsed().as_millis() as u64;
{
let mut stats = self.stats.lock();
stats.verify_time_ms += verify_elapsed;
stats.target_forward_passes += 1;
stats.accepted_tokens += accepted as u64;
stats.rejected_tokens += (draft_tokens.len() - accepted) as u64;
}
for token in &draft_tokens[..accepted] {
if *token == eos_token {
return Ok(generated);
}
generated.push(*token);
}
if let Some(token) = next_token {
if token == eos_token {
return Ok(generated);
}
generated.push(token);
}
current_pos = prompt_tokens.len() + generated.len();
if self.stats.lock().rounds % 10 == 0 {
let stats = self.stats.lock();
println!(
" [Round {}] {} tokens, {:.1}% accepted, {:.1} tok/round",
stats.rounds,
generated.len(),
stats.acceptance_rate() * 100.0,
stats.tokens_per_round()
);
}
}
Ok(generated)
}
fn generate_draft_tokens(
&self,
prompt_tokens: &[u32],
generated: &[u32],
num_tokens: usize,
eos_token: u32,
) -> anyhow::Result<Vec<u32>> {
let mut draft = self.draft.lock();
let mut draft_tokens = Vec::with_capacity(num_tokens);
let context: Vec<u32> = prompt_tokens
.iter()
.chain(generated.iter())
.copied()
.collect();
let base_pos = context.len();
for i in 0..num_tokens {
let input_token = if i == 0 {
*context.last().unwrap_or(&0)
} else {
draft_tokens[i - 1]
};
let input = Tensor::new(&[input_token], &self.device)?.unsqueeze(0)?;
let logits = draft.forward(&input, base_pos + i - 1)?;
let last_logits = logits.i((0, 0, ..))?.to_dtype(DType::F32)?;
let next_token = if self.config.greedy_draft {
last_logits.argmax(0)?.to_scalar::<u32>()?
} else {
let scaled = (&last_logits / self.config.draft_temperature as f64)?;
let probs = candle_nn::ops::softmax(&scaled, 0)?;
sample_from_probs(&probs)?
};
if next_token == eos_token {
draft_tokens.push(next_token);
break;
}
draft_tokens.push(next_token);
}
Ok(draft_tokens)
}
fn verify_draft_tokens(
&self,
prompt_tokens: &[u32],
generated: &[u32],
draft_tokens: &[u32],
base_pos: usize,
) -> anyhow::Result<(usize, Option<u32>)> {
if draft_tokens.is_empty() {
return Ok((0, None));
}
let mut target = self.target.lock();
let last_context = if generated.is_empty() {
*prompt_tokens.last().unwrap_or(&0)
} else {
*generated.last().unwrap_or(&0)
};
let input_tokens: Vec<u32> = std::iter::once(last_context)
.chain(draft_tokens.iter().copied())
.collect();
let input = Tensor::new(input_tokens.as_slice(), &self.device)?.unsqueeze(0)?;
let logits = target.forward(&input, base_pos - 1)?;
let threshold = self.config.acceptance_threshold;
let mut accepted = 0;
for (i, &draft_token) in draft_tokens.iter().enumerate() {
let pos_logits = logits.i((0, i, ..))?.to_dtype(DType::F32)?;
if self.config.greedy_draft {
let target_token = pos_logits.argmax(0)?.to_scalar::<u32>()?;
if target_token == draft_token {
accepted += 1;
} else {
return Ok((accepted, Some(target_token)));
}
} else {
let probs = candle_nn::ops::softmax(&pos_logits, 0)?;
let probs_vec: Vec<f32> = probs.to_vec1()?;
let draft_prob = probs_vec.get(draft_token as usize).copied().unwrap_or(0.0);
let max_prob = probs_vec.iter().fold(0.0_f32, |a, &b| a.max(b));
if draft_prob >= threshold * max_prob {
accepted += 1;
} else {
let next_token = sample_from_probs(&probs)?;
return Ok((accepted, Some(next_token)));
}
}
}
let next_pos_logits = logits
.i((0, draft_tokens.len(), ..))?
.to_dtype(DType::F32)?;
let next_token = if self.config.greedy_draft {
next_pos_logits.argmax(0)?.to_scalar::<u32>()?
} else {
let probs = candle_nn::ops::softmax(&next_pos_logits, 0)?;
sample_from_probs(&probs)?
};
Ok((accepted, Some(next_token)))
}
}
fn sample_from_probs(probs: &Tensor) -> anyhow::Result<u32> {
let probs_vec: Vec<f32> = probs.to_vec1()?;
let r: f32 = fastrand::f32();
let mut cumsum = 0.0;
for (i, &p) in probs_vec.iter().enumerate() {
cumsum += p;
if cumsum >= r {
return Ok(i as u32);
}
}
Ok((probs_vec.len() - 1) as u32)
}
impl DraftModel for crate::models::qwen2::Qwen2 {
fn forward(&mut self, input_ids: &Tensor, pos: usize) -> candle_core::Result<Tensor> {
crate::models::qwen2::Qwen2::forward(self, input_ids, pos)
}
fn clear_cache(&mut self) {
crate::models::qwen2::Qwen2::clear_cache(self);
}
fn device(&self) -> &Device {
crate::models::qwen2::Qwen2::device(self)
}
fn dtype(&self) -> DType {
crate::models::qwen2::Qwen2::dtype(self)
}
}
impl DraftModel for crate::models::llama::Llama {
fn forward(&mut self, input_ids: &Tensor, pos: usize) -> candle_core::Result<Tensor> {
crate::models::llama::Llama::forward(self, input_ids, pos)
}
fn clear_cache(&mut self) {
crate::models::llama::Llama::clear_cache(self);
}
fn device(&self) -> &Device {
crate::models::llama::Llama::device(self)
}
fn dtype(&self) -> DType {
crate::models::llama::Llama::dtype(self)
}
}
impl TargetModel for crate::models::lazy_llama::LazyLlama {
fn forward(&mut self, input_ids: &Tensor, pos: usize) -> anyhow::Result<Tensor> {
crate::models::lazy_llama::LazyLlama::forward(self, input_ids, pos)
.map_err(|e| anyhow::anyhow!("LazyLlama forward error: {}", e))
}
fn clear_cache(&mut self) {
crate::models::lazy_llama::LazyLlama::clear_cache(self);
}
}
impl TargetModel for crate::models::lazy_qwen2::LazyQwen2 {
fn forward(&mut self, input_ids: &Tensor, pos: usize) -> anyhow::Result<Tensor> {
crate::models::lazy_qwen2::LazyQwen2::forward(self, input_ids, pos)
.map_err(|e| anyhow::anyhow!("LazyQwen2 forward error: {}", e))
}
fn clear_cache(&mut self) {
crate::models::lazy_qwen2::LazyQwen2::clear_cache(self);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = Speculative405BConfig::default();
assert_eq!(config.num_draft_tokens, 5);
assert!(config.greedy_draft);
}
#[test]
fn test_config_fast() {
let config = Speculative405BConfig::fast();
assert_eq!(config.num_draft_tokens, 8);
assert!(config.acceptance_threshold < 0.1);
}
#[test]
fn test_stats_acceptance_rate() {
let mut stats = Speculative405BStats::default();
stats.draft_tokens = 100;
stats.accepted_tokens = 80;
assert!((stats.acceptance_rate() - 0.8).abs() < 0.001);
}
#[test]
fn test_stats_speedup() {
let mut stats = Speculative405BStats::default();
stats.rounds = 25;
stats.accepted_tokens = 75;
stats.target_forward_passes = 25;
assert!((stats.speedup() - 4.0).abs() < 0.001);
}
}