use std::time::Instant;
use foundation::{LogitValue, LogitsView, TokenId};
use selection::derive::{derive_step_value, to_unit_interval};
use selection::{residual, select, shaped_distribution, ShapedDistribution};
use tokenizer::MfTokenizer;
use crate::config::GenerationConfig;
use crate::error::RuntimeError;
use crate::producer::SpeculativeProducer;
use crate::raw_completion::{
CancelFlag, RawDecodeProgress, RawDecodeResult, StopReason, TokenSink, NEVER,
};
pub const DEFAULT_SPECULATION_BLOCK: usize = 2;
#[allow(clippy::too_many_arguments)]
pub fn run_raw_completion_speculative<P: SpeculativeProducer>(
producer: &mut P,
tokenizer: &MfTokenizer,
prompt_ids: &[TokenId],
config: &GenerationConfig,
max_context: u32,
vocab_size: usize,
block: usize,
on_progress: impl FnMut(RawDecodeProgress),
) -> Result<RawDecodeResult, RuntimeError> {
run_raw_completion_speculative_cancellable(
producer,
tokenizer,
prompt_ids,
config,
max_context,
vocab_size,
block,
NEVER,
on_progress,
)
}
#[allow(clippy::too_many_arguments)]
pub fn run_raw_completion_speculative_cancellable<P: SpeculativeProducer>(
producer: &mut P,
tokenizer: &MfTokenizer,
prompt_ids: &[TokenId],
config: &GenerationConfig,
max_context: u32,
vocab_size: usize,
block: usize,
cancel: CancelFlag<'_>,
mut on_progress: impl FnMut(RawDecodeProgress),
) -> Result<RawDecodeResult, RuntimeError> {
crate::raw_completion::check_admission(prompt_ids, config, max_context)?;
if block == 0 {
return Err(RuntimeError::SpeculationUnavailable(
"block size 0 proposes nothing; call run_raw_completion instead".to_string(),
));
}
if !config.shaping.is_deterministic() && producer.drafts_block_passes() {
return Err(RuntimeError::SpeculationUnavailable(
"sampled speculation cannot be served by the block drafter: DFlash2's selection is \
a greedy structured search, not a distribution, so exact rejection sampling has \
no q(x) to ratio against; use the mtp drafter or temperature 0"
.to_string(),
));
}
let reused = producer
.try_reuse_prefix(prompt_ids)
.min(prompt_ids.len() - 1);
if reused == 0 {
producer.reset();
}
let mut logits = vec![LogitValue::from_f32(0.0); vocab_size];
let mut draft_logits = vec![LogitValue::from_f32(0.0); vocab_size];
let mut batch_logits = vec![LogitValue::from_f32(0.0); (block + 1) * vocab_size];
let mut history: Vec<TokenId> =
Vec::with_capacity(prompt_ids.len() + config.max_new_tokens as usize);
let headless = producer.supports_headless_prefill();
let prefill_start = Instant::now();
let mut position = reused;
history.extend_from_slice(&prompt_ids[..reused]);
for (i, &token) in prompt_ids.iter().enumerate().skip(reused) {
let last = i + 1 == prompt_ids.len();
if last || !headless {
producer
.produce(token, position, &mut logits)
.map_err(RuntimeError::Producer)?;
} else {
producer
.produce_prefill(token, position, &mut logits)
.map_err(RuntimeError::Producer)?;
}
if i + 1 < prompt_ids.len() {
producer
.prime_drafter(prompt_ids[i + 1], i)
.map_err(RuntimeError::Producer)?;
}
position += 1;
history.push(token);
on_progress(RawDecodeProgress::Prefill {
done: position,
total: prompt_ids.len(),
});
if cancel() {
return Ok(crate::raw_completion::cancelled_during_prefill(
history,
position,
prompt_ids.len(),
prefill_start,
reused,
producer.session_slot_evicted(),
));
}
}
let prefill_seconds = prefill_start.elapsed().as_secs_f64();
let decode_start = Instant::now();
let mut sink = TokenSink::new(tokenizer, config, history);
let mut proposals: Vec<TokenId> = Vec::with_capacity(block);
let mut draft_qs: Vec<ShapedDistribution> = Vec::with_capacity(block);
let mut feed: Vec<TokenId> = Vec::with_capacity(block + 1);
let reason;
let sampled = !config.shaping.is_deterministic();
let mut draw_step: u64 = 0;
let mut next = select(
LogitsView::new(&logits),
&config.shaping,
&sink.history,
sink.generated as u64,
)?;
let stats = std::env::var("TURBOSPARK_SPEC_STATS").as_deref() == Ok("1");
let mut stat_rounds = 0usize;
let mut stat_accepted = 0usize;
let mut stat_offered = vec![0usize; block];
let mut stat_matched = vec![0usize; block];
let mut stat_rollbacks = 0usize;
'rounds: loop {
if let Some(stop) = sink.commit(next, cancel, &mut on_progress) {
reason = stop;
break;
}
let base = sink.history.len() - 1;
position = sink.history.len();
let budget_room = (config.max_new_tokens as usize).saturating_sub(sink.generated);
let context_room = (max_context as usize).saturating_sub(position);
let round_block = block.min(budget_room).min(context_room);
if round_block == 0 {
return Err(RuntimeError::Producer(
"round_block is 0: no room after admission; invariant violation".to_string(),
));
}
proposals.clear();
draft_qs.clear();
if producer.drafts_block_passes() {
producer
.draft_block(next, base, round_block, &mut proposals)
.map_err(RuntimeError::Producer)?;
assert_eq!(
proposals.len(),
round_block,
"draft_block must populate exactly round_block proposals ({round_block} expected, {} produced)",
proposals.len()
);
} else {
let mut chained = next;
for d in 0..=round_block {
producer
.draft_step(chained, base - 1 + d, &mut draft_logits)
.map_err(RuntimeError::Producer)?;
if sampled {
let q = shaped_distribution(
LogitsView::new(&draft_logits),
&config.shaping,
&sink.history,
sink.generated as u64,
)?;
chained = q.draw(config.shaping.seed(), draw_step) as TokenId;
draw_step += 1;
if d < round_block {
proposals.push(chained);
draft_qs.push(q);
}
} else {
chained = select(
LogitsView::new(&draft_logits),
&config.shaping,
&sink.history,
sink.generated as u64,
)?;
if d < round_block {
proposals.push(chained);
}
}
}
}
feed.clear();
feed.push(next);
feed.extend_from_slice(&proposals);
let point = producer.checkpoint();
producer
.verify(&feed, base, &mut batch_logits[..feed.len() * vocab_size])
.map_err(RuntimeError::Producer)?;
let mut accepted = 0usize;
let mut stopped: Option<StopReason> = None;
let mut corrected: Option<TokenId> = None;
stat_rounds += 1;
for (i, &proposal) in proposals.iter().enumerate() {
if stats {
stat_offered[i] += 1;
}
let row = &batch_logits[i * vocab_size..(i + 1) * vocab_size];
if sampled {
let p = shaped_distribution(
LogitsView::new(row),
&config.shaping,
&sink.history,
sink.generated as u64,
)?;
let q = &draft_qs[i];
let r = to_unit_interval(derive_step_value(config.shaping.seed(), draw_step));
draw_step += 1;
if r * q.probability(proposal as u32) <= p.probability(proposal as u32) {
if stats {
stat_matched[i] += 1;
}
accepted += 1;
if let Some(stop) = sink.commit(proposal, cancel, &mut on_progress) {
stopped = Some(stop);
break;
}
} else {
corrected = Some(match residual(&p, q) {
Some(residual_p) => residual_p.draw(config.shaping.seed(), draw_step),
None => p.draw(config.shaping.seed(), draw_step),
} as TokenId);
draw_step += 1;
break;
}
} else {
let target = select(
LogitsView::new(row),
&config.shaping,
&sink.history,
sink.generated as u64,
)?;
if target != proposal {
break;
}
if stats {
stat_matched[i] += 1;
}
accepted += 1;
if let Some(stop) = sink.commit(proposal, cancel, &mut on_progress) {
stopped = Some(stop);
break;
}
}
}
stat_accepted += accepted;
let committed = sink.history.len() - (base + 1);
if committed < proposals.len() {
stat_rollbacks += 1;
if producer.supports_retaining_rollback() {
producer
.rollback_retaining(&point, committed + 1)
.map_err(RuntimeError::Producer)?;
} else {
producer.rollback(&point);
let keep = committed + 1;
producer
.verify(&feed[..keep], base, &mut batch_logits[..keep * vocab_size])
.map_err(RuntimeError::Producer)?;
}
}
position = sink.history.len();
producer.record_committed(&feed[..committed + 1], base);
producer
.rewind_drafter(base + committed)
.map_err(RuntimeError::Producer)?;
if let Some(stop) = stopped {
reason = stop;
break 'rounds;
}
if let Some(token) = corrected {
next = token;
} else if sampled {
let p = shaped_distribution(
LogitsView::new(&batch_logits[accepted * vocab_size..(accepted + 1) * vocab_size]),
&config.shaping,
&sink.history,
sink.generated as u64,
)?;
next = p.draw(config.shaping.seed(), draw_step) as TokenId;
draw_step += 1;
} else {
next = select(
LogitsView::new(&batch_logits[accepted * vocab_size..(accepted + 1) * vocab_size]),
&config.shaping,
&sink.history,
sink.generated as u64,
)?;
}
}
if stats {
let per_round = stat_accepted as f64 / stat_rounds.max(1) as f64;
eprintln!(
"[spec-stats] rounds={stat_rounds} accepted/round={per_round:.2} \
committed/round={:.2} rollbacks={stat_rollbacks}",
per_round + 1.0
);
let curve: Vec<String> = stat_offered
.iter()
.zip(&stat_matched)
.map(|(o, m)| {
if *o == 0 {
"-".to_string()
} else {
format!("{:.2}", *m as f64 / *o as f64)
}
})
.collect();
eprintln!("[spec-stats] per-position acceptance: {}", curve.join(" "));
}
Ok(RawDecodeResult {
reused_prefix_tokens: reused,
session_slot_evicted: producer.session_slot_evicted(),
prompt_tokens: prompt_ids.len(),
new_tokens: sink.generated,
prefill_seconds,
decode_seconds: decode_start.elapsed().as_secs_f64(),
reason,
kv_position: position,
kv_backed_token_ids: sink.history,
peak_memory_pressure: crate::power::MemoryPressure::Normal,
})
}