use crate::{
backend::{
Completion, ModelRuntime, SpeculativeTokenFilterController, Submission,
TextGenerationBackend, TextGenerationConfig,
},
generation::{
FinishReason, GenerationCancellationToken, GenerationError, GenerationSequence,
SemanticEvent, SpeculativeCancellationDisposition, SpeculativeConfig, SpeculativeRequestId,
SpeculativeRequestLifecycle, SpeculativeRequestStatus, SpeculativeRound,
SpeculativeSchedulerOptions, TokenTerminalSignals,
},
};
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
#[non_exhaustive]
pub enum SpeculativeDraft<'a, D> {
External(&'a mut D),
Embedded,
}
pub struct SpeculativeGenerationOutput {
token_ids: Vec<u32>,
finish_reason: FinishReason,
stats: SpeculativeStats,
}
impl SpeculativeGenerationOutput {
pub fn new(token_ids: Vec<u32>, finish_reason: FinishReason, stats: SpeculativeStats) -> Self {
Self {
token_ids,
finish_reason,
stats,
}
}
pub fn token_ids(&self) -> &[u32] {
&self.token_ids
}
pub const fn finish_reason(&self) -> FinishReason {
self.finish_reason
}
pub const fn stats(&self) -> &SpeculativeStats {
&self.stats
}
}
pub struct SpeculativeGenerationBatchOutput {
requests: Vec<SpeculativeGenerationOutput>,
scheduler: SpeculativeSchedulerStats,
}
impl SpeculativeGenerationBatchOutput {
pub fn new(
requests: Vec<SpeculativeGenerationOutput>,
scheduler: SpeculativeSchedulerStats,
) -> Self {
Self {
requests,
scheduler,
}
}
pub fn requests(&self) -> &[SpeculativeGenerationOutput] {
&self.requests
}
pub fn into_requests(self) -> Vec<SpeculativeGenerationOutput> {
self.requests
}
pub const fn scheduler(&self) -> &SpeculativeSchedulerStats {
&self.scheduler
}
pub fn push_request(&mut self, request: SpeculativeGenerationOutput) {
self.requests.push(request);
}
pub fn clear_requests(&mut self) {
self.requests.clear();
}
}
pub struct SpeculativeGenerationLane<'a, B, C>
where
B: TextGenerationBackend,
C: SpeculativeTokenFilterController,
{
prompt: Option<B::Prompt>,
generation: Option<TextGenerationConfig>,
config: Option<SpeculativeConfig>,
constraint: Option<C>,
semantic: Option<Box<dyn SpeculativeSemanticState>>,
cancellation: Option<GenerationCancellationToken>,
on_event: Option<Box<dyn FnMut(SemanticEvent) + 'a>>,
}
impl<'a, B, C> SpeculativeGenerationLane<'a, B, C>
where
B: TextGenerationBackend,
C: SpeculativeTokenFilterController,
{
#[allow(clippy::too_many_arguments)]
pub fn new(
prompt: B::Prompt,
generation: TextGenerationConfig,
config: SpeculativeConfig,
constraint: C,
semantic: Box<dyn SpeculativeSemanticState>,
cancellation: GenerationCancellationToken,
on_event: Box<dyn FnMut(SemanticEvent) + 'a>,
) -> Self {
Self {
prompt: Some(prompt),
generation: Some(generation),
config: Some(config),
constraint: Some(constraint),
semantic: Some(semantic),
cancellation: Some(cancellation),
on_event: Some(on_event),
}
}
pub fn take_prompt(&mut self) -> B::Prompt {
self.prompt.take().expect("lane prompt already taken")
}
pub fn prompt(&self) -> &B::Prompt {
self.prompt.as_ref().expect("lane prompt already taken")
}
pub fn take_generation(&mut self) -> TextGenerationConfig {
self.generation
.take()
.expect("lane generation already taken")
}
pub fn generation(&self) -> &TextGenerationConfig {
self.generation
.as_ref()
.expect("lane generation already taken")
}
pub fn take_config(&mut self) -> SpeculativeConfig {
self.config.take().expect("lane config already taken")
}
pub fn config(&self) -> &SpeculativeConfig {
self.config.as_ref().expect("lane config already taken")
}
pub fn take_constraint(&mut self) -> C {
self.constraint
.take()
.expect("lane constraint already taken")
}
pub fn take_semantic(&mut self) -> Box<dyn SpeculativeSemanticState> {
self.semantic
.take()
.expect("lane semantic state already taken")
}
pub fn take_cancellation(&mut self) -> GenerationCancellationToken {
self.cancellation
.take()
.expect("lane cancellation already taken")
}
pub fn take_on_event(&mut self) -> Box<dyn FnMut(SemanticEvent) + 'a> {
self.on_event
.take()
.expect("lane event callback already taken")
}
}
pub struct SpeculativeGenerationBatchRequest<'a, B, D, C>
where
B: TextGenerationBackend,
C: SpeculativeTokenFilterController,
{
drafting: Option<SpeculativeDraft<'a, D>>,
lanes: Option<Vec<SpeculativeGenerationLane<'a, B, C>>>,
tokenizer_fingerprint: [u8; 32],
}
impl<'a, B, D, C> SpeculativeGenerationBatchRequest<'a, B, D, C>
where
B: TextGenerationBackend,
C: SpeculativeTokenFilterController,
{
pub fn new(
drafting: SpeculativeDraft<'a, D>,
lanes: Vec<SpeculativeGenerationLane<'a, B, C>>,
tokenizer_fingerprint: [u8; 32],
) -> Self {
Self {
drafting: Some(drafting),
lanes: Some(lanes),
tokenizer_fingerprint,
}
}
pub const fn tokenizer_fingerprint(&self) -> [u8; 32] {
self.tokenizer_fingerprint
}
pub fn take_drafting(&mut self) -> SpeculativeDraft<'a, D> {
self.drafting.take().expect("draft selection already taken")
}
pub fn take_lanes(&mut self) -> Vec<SpeculativeGenerationLane<'a, B, C>> {
self.lanes.take().expect("speculative lanes already taken")
}
}
pub trait SpeculativeGenerationBackend: TextGenerationBackend {
type Drafter;
fn speculative_capability(runtime: &ModelRuntime<Self>) -> SpeculativeCapability;
fn with_speculative_execution<C, V>(
runtime: &mut ModelRuntime<Self>,
request: SpeculativeGenerationBatchRequest<'_, Self, Self::Drafter, C>,
visitor: V,
) -> Result<SpeculativeGenerationBatchOutput, Self::Error>
where
C: SpeculativeTokenFilterController,
V: SpeculativeGenerationVisitor;
}
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SpeculativeExecutionTopology {
#[default]
Single,
SameDeviceSplit,
CrossDeviceSplit,
}
impl std::fmt::Display for SpeculativeExecutionTopology {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Single => "single",
Self::SameDeviceSplit => "same-device-split",
Self::CrossDeviceSplit => "cross-device-split",
})
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SpeculativeDraftSource {
Separate,
Embedded,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SpeculativeCapability {
Unavailable,
Ready {
draft_source: SpeculativeDraftSource,
},
Unsupported {
draft_source: SpeculativeDraftSource,
architecture: String,
},
}
#[derive(Debug, Clone, Default)]
pub struct SpeculativeStats {
execution_topology: SpeculativeExecutionTopology,
target_tokens: usize,
draft_tokens: usize,
accepted_tokens: usize,
rounds: usize,
accept_lens: Vec<usize>,
emitted_tokens: usize,
optimistic_draft_tokens: usize,
optimistic_draft_blocks: usize,
reused_optimistic_tokens: usize,
reused_optimistic_blocks: usize,
consumed_optimistic_tokens: usize,
discarded_optimistic_tokens: usize,
discarded_optimistic_blocks: usize,
optimistic_target_bonus_tokens: usize,
optimistic_bonus_matches: usize,
optimistic_bonus_mismatches: usize,
adaptive_lookahead_disabled: bool,
optimistic_draft_time: Duration,
verification_in_flight_time: Duration,
component_timings_collected: bool,
draft_context_time: Duration,
draft_assistant_time: Duration,
draft_head_time: Duration,
target_verification_time: Duration,
scheduler_turns: usize,
cross_request_draft_opportunities: usize,
elapsed: Duration,
}
impl SpeculativeStats {
pub const fn execution_topology(&self) -> SpeculativeExecutionTopology {
self.execution_topology
}
pub const fn target_tokens(&self) -> usize {
self.target_tokens
}
pub const fn draft_tokens(&self) -> usize {
self.draft_tokens
}
pub const fn accepted_tokens(&self) -> usize {
self.accepted_tokens
}
pub const fn rounds(&self) -> usize {
self.rounds
}
pub fn accept_lens(&self) -> &[usize] {
&self.accept_lens
}
pub const fn emitted_tokens(&self) -> usize {
self.emitted_tokens
}
pub const fn optimistic_draft_tokens(&self) -> usize {
self.optimistic_draft_tokens
}
pub const fn optimistic_draft_blocks(&self) -> usize {
self.optimistic_draft_blocks
}
pub const fn reused_optimistic_tokens(&self) -> usize {
self.reused_optimistic_tokens
}
pub const fn reused_optimistic_blocks(&self) -> usize {
self.reused_optimistic_blocks
}
pub const fn consumed_optimistic_tokens(&self) -> usize {
self.consumed_optimistic_tokens
}
pub const fn discarded_optimistic_tokens(&self) -> usize {
self.discarded_optimistic_tokens
}
pub const fn discarded_optimistic_blocks(&self) -> usize {
self.discarded_optimistic_blocks
}
pub const fn optimistic_target_bonus_tokens(&self) -> usize {
self.optimistic_target_bonus_tokens
}
pub const fn optimistic_bonus_matches(&self) -> usize {
self.optimistic_bonus_matches
}
pub const fn optimistic_bonus_mismatches(&self) -> usize {
self.optimistic_bonus_mismatches
}
pub const fn adaptive_lookahead_disabled(&self) -> bool {
self.adaptive_lookahead_disabled
}
pub const fn optimistic_draft_time(&self) -> Duration {
self.optimistic_draft_time
}
pub const fn verification_in_flight_time(&self) -> Duration {
self.verification_in_flight_time
}
pub const fn component_timings_collected(&self) -> bool {
self.component_timings_collected
}
pub const fn draft_context_time(&self) -> Duration {
self.draft_context_time
}
pub const fn draft_assistant_time(&self) -> Duration {
self.draft_assistant_time
}
pub const fn draft_head_time(&self) -> Duration {
self.draft_head_time
}
pub const fn target_verification_time(&self) -> Duration {
self.target_verification_time
}
pub const fn scheduler_turns(&self) -> usize {
self.scheduler_turns
}
pub const fn cross_request_draft_opportunities(&self) -> usize {
self.cross_request_draft_opportunities
}
pub const fn elapsed(&self) -> Duration {
self.elapsed
}
pub fn add_component_timings(
&mut self,
draft_context: Duration,
draft_assistant: Duration,
draft_head: Duration,
target_verification: Duration,
) {
self.draft_context_time += draft_context;
self.draft_assistant_time += draft_assistant;
self.draft_head_time += draft_head;
self.target_verification_time += target_verification;
self.component_timings_collected = true;
}
pub fn add_scheduler_rounds(&mut self, rounds: usize) {
self.rounds += rounds;
}
pub fn record_optimistic_accounting(
&mut self,
drafted_blocks: usize,
reused_tokens: usize,
discarded_tokens: usize,
) {
self.optimistic_draft_blocks += drafted_blocks;
self.reused_optimistic_tokens += reused_tokens;
self.discarded_optimistic_tokens += discarded_tokens;
}
pub fn reset_adaptive_lookahead_decision(&mut self) {
self.adaptive_lookahead_disabled = false;
}
pub fn accept_rate(&self) -> f64 {
if self.draft_tokens == 0 {
0.0
} else {
self.accepted_tokens as f64 / self.draft_tokens as f64
}
}
pub fn update_adaptive_lookahead(&mut self, options: SpeculativeSchedulerOptions) {
if !options.adaptive_lookahead
|| self.adaptive_lookahead_disabled
|| self.optimistic_draft_blocks < options.adaptive_lookahead_min_blocks
{
return;
}
self.adaptive_lookahead_disabled = self.reused_optimistic_tokens == 0
|| self.reused_optimistic_tokens < self.discarded_optimistic_tokens;
}
}
#[derive(Debug, Clone, Default)]
pub struct SpeculativeSchedulerStats {
execution_topology: SpeculativeExecutionTopology,
turns: usize,
cross_request_draft_opportunities: usize,
peak_in_flight_verifications: usize,
peak_optimistic_branches: usize,
}
impl SpeculativeSchedulerStats {
pub const fn execution_topology(&self) -> SpeculativeExecutionTopology {
self.execution_topology
}
pub const fn turns(&self) -> usize {
self.turns
}
pub const fn cross_request_draft_opportunities(&self) -> usize {
self.cross_request_draft_opportunities
}
pub const fn peak_in_flight_verifications(&self) -> usize {
self.peak_in_flight_verifications
}
pub const fn peak_optimistic_branches(&self) -> usize {
self.peak_optimistic_branches
}
}
pub trait SpeculativeTelemetry: Default {
fn record(self, stats: &mut SpeculativeStats);
}
impl SpeculativeTelemetry for () {
fn record(self, _stats: &mut SpeculativeStats) {}
}
#[derive(Debug)]
pub struct SpeculativePrefill<State, Logits> {
logits: Logits,
state: State,
evaluated_tokens: usize,
}
impl<State, Logits> SpeculativePrefill<State, Logits> {
pub const fn new(logits: Logits, state: State, evaluated_tokens: usize) -> Self {
Self {
logits,
state,
evaluated_tokens,
}
}
}
#[derive(Debug)]
pub struct SpeculativeCommit<State> {
state: State,
replayed_tokens: usize,
}
impl<State> SpeculativeCommit<State> {
pub const fn new(state: State, replayed_tokens: usize) -> Self {
Self {
state,
replayed_tokens,
}
}
}
pub trait SpeculativeExecutor {
type Input;
type Cache;
type TargetState;
type DraftState: Clone;
type CacheCheckpoint;
type Verification;
type Logits;
type Context<'a>: Copy
where
Self: 'a;
type Completion: Completion<Error = Self::Error>;
type Telemetry: SpeculativeTelemetry;
type Error: std::error::Error + Send + Sync + 'static;
fn max_proposals(&self) -> usize {
usize::MAX
}
fn set_telemetry_enabled(&mut self, _enabled: bool) {}
fn supports_telemetry(&self) -> bool {
false
}
fn take_telemetry(&mut self) -> Result<Self::Telemetry, Self::Error> {
Ok(Self::Telemetry::default())
}
fn take_verification_telemetry(
&mut self,
_output: &mut Self::Verification,
) -> Result<Self::Telemetry, Self::Error> {
Ok(Self::Telemetry::default())
}
fn supports_exact_optimistic_promotion(&self) -> bool {
false
}
fn prefill<'context>(
&mut self,
input: Self::Input,
cache: &mut Self::Cache,
context: Self::Context<'context>,
) -> Result<SpeculativePrefill<Self::TargetState, Self::Logits>, Self::Error>
where
Self: 'context;
fn begin_proposal<'a>(
&mut self,
state: &Self::TargetState,
last_token: u32,
proposal_capacity: usize,
context: Self::Context<'a>,
) -> Result<Self::DraftState, Self::Error>;
fn proposal_logits<'a>(
&mut self,
state: &mut Self::DraftState,
last_token: u32,
context: Self::Context<'a>,
) -> Result<Self::Logits, Self::Error>;
fn checkpoint(cache: &Self::Cache) -> Self::CacheCheckpoint;
fn submit_verification<'a>(
&mut self,
input_tokens: &[u32],
cache: &mut Self::Cache,
context: Self::Context<'a>,
) -> Result<Submission<Self::Verification, Self::Completion>, Self::Error>;
fn verification_logits<'a>(
output: &Self::Verification,
index: usize,
context: Self::Context<'a>,
) -> Result<Self::Logits, Self::Error>
where
Self: 'a;
fn commit_verification<'a>(
&mut self,
output: Self::Verification,
draft_state: Self::DraftState,
cache: &mut Self::Cache,
checkpoint: Self::CacheCheckpoint,
verified_inputs: usize,
context: Self::Context<'a>,
) -> Result<SpeculativeCommit<Self::TargetState>, Self::Error>;
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum ProposalDecision {
Accept,
Reject(u32),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum SamplingPlacement {
Target,
Draft,
}
#[derive(Debug, Clone)]
pub struct SpeculativeRandomness<R, D> {
target: Option<R>,
draft: Option<D>,
}
impl<R, D> SpeculativeRandomness<R, D> {
pub const fn new(target: Option<R>, draft: Option<D>) -> Self {
Self { target, draft }
}
}
pub struct PreparedSpeculativeLane<'a, E, S, C, P>
where
E: SpeculativeExecutor,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error>,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
cache: Option<&'a mut E::Cache>,
input: Option<E::Input>,
config: Option<SpeculativeConfig>,
runtime: Option<SpeculativeOutputRuntime<S, C, P>>,
randomness: Option<SpeculativeRandomness<S::RandomState, S::DraftRandomness>>,
}
impl<'a, E, S, C, P> PreparedSpeculativeLane<'a, E, S, C, P>
where
E: SpeculativeExecutor,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error>,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
pub fn new(
cache: &'a mut E::Cache,
input: E::Input,
config: SpeculativeConfig,
runtime: SpeculativeOutputRuntime<S, C, P>,
randomness: SpeculativeRandomness<S::RandomState, S::DraftRandomness>,
) -> Self {
Self {
cache: Some(cache),
input: Some(input),
config: Some(config),
runtime: Some(runtime),
randomness: Some(randomness),
}
}
pub fn take_cache(&mut self) -> &'a mut E::Cache {
self.cache.take().expect("prepared cache already taken")
}
pub fn take_input(&mut self) -> E::Input {
self.input.take().expect("prepared input already taken")
}
pub fn take_config(&mut self) -> SpeculativeConfig {
self.config.take().expect("prepared config already taken")
}
pub fn take_runtime(&mut self) -> SpeculativeOutputRuntime<S, C, P> {
self.runtime.take().expect("prepared runtime already taken")
}
pub fn take_randomness(&mut self) -> SpeculativeRandomness<S::RandomState, S::DraftRandomness> {
self.randomness
.take()
.expect("prepared randomness already taken")
}
}
pub trait SpeculativeGenerationVisitor {
#[allow(clippy::too_many_arguments)]
fn run<'a, E, S, C, P>(
self,
executor: &'a mut E,
lanes: Vec<PreparedSpeculativeLane<'a, E, S, C, P>>,
topology: SpeculativeExecutionTopology,
optimistic_execution_available: bool,
component_timings_collected: bool,
context: E::Context<'a>,
) -> Result<SpeculativeGenerationBatchOutput, SpeculativeDriverError<E::Error>>
where
E: SpeculativeExecutor + 'a,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error, Context<'a> = E::Context<'a>>
+ 'a,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>;
}
pub trait SpeculativeSampling: Clone {
type Logits;
type Distribution;
type Seed;
type RandomState: Clone;
type DraftRandomness: Clone;
type Context<'a>: Copy
where
Self: 'a;
type Error: std::error::Error + Send + Sync + 'static;
fn supports_exact_optimistic_promotion(&self) -> bool {
false
}
fn grammar_is_complete(&mut self) -> Result<bool, Self::Error> {
Ok(false)
}
fn prefix_is_complete(&self, _history: &[u32]) -> Result<bool, Self::Error> {
Ok(false)
}
fn initialize_randomness<'a>(
seed: Option<Self::Seed>,
temperature: f32,
context: Self::Context<'a>,
) -> Result<SpeculativeRandomness<Self::RandomState, Self::DraftRandomness>, Self::Error>
where
Self: 'a;
fn draft_randomness_at<'a>(
root: &Self::DraftRandomness,
position: usize,
context: Self::Context<'a>,
) -> Result<Self::RandomState, Self::Error>
where
Self: 'a;
fn process_logits<'a>(
&mut self,
logits: &Self::Logits,
temperature: f32,
history: &[u32],
placement: SamplingPlacement,
context: Self::Context<'a>,
) -> Result<Self::Distribution, Self::Error>
where
Self: 'a;
fn sample<'a>(
&self,
distribution: &Self::Distribution,
temperature: f32,
randomness: Option<&mut Self::RandomState>,
placement: SamplingPlacement,
context: Self::Context<'a>,
) -> Result<u32, Self::Error>
where
Self: 'a;
fn decide_proposal<'a>(
&self,
target: &Self::Distribution,
draft: &Self::Distribution,
proposed: u32,
temperature: f32,
randomness: Option<&mut Self::RandomState>,
context: Self::Context<'a>,
) -> Result<ProposalDecision, Self::Error>
where
Self: 'a;
fn commit_token<'a>(
&mut self,
distribution: &Self::Distribution,
token: u32,
placement: SamplingPlacement,
context: Self::Context<'a>,
) -> Result<(), Self::Error>
where
Self: 'a;
fn prepare_verification<'a>(
&self,
_distributions: &mut [&mut Self::Distribution],
_temperature: f32,
_context: Self::Context<'a>,
) -> Result<(), Self::Error>
where
Self: 'a,
{
Ok(())
}
}
#[derive(Debug)]
pub struct SpeculativeProposal<D> {
token: u32,
distribution: D,
}
impl<D> SpeculativeProposal<D> {
pub const fn new(token: u32, distribution: D) -> Self {
Self {
token,
distribution,
}
}
pub const fn token(&self) -> u32 {
self.token
}
pub const fn distribution(&self) -> &D {
&self.distribution
}
}
pub struct SpeculativeDraftBlock<S, D> {
state: S,
proposals: Vec<SpeculativeProposal<D>>,
}
impl<S, D> SpeculativeDraftBlock<S, D> {
pub fn new(state: S, proposals: Vec<SpeculativeProposal<D>>) -> Self {
Self { state, proposals }
}
pub const fn state(&self) -> &S {
&self.state
}
pub fn proposals(&self) -> &[SpeculativeProposal<D>] {
&self.proposals
}
}
pub struct SpeculativeOptimisticBranch<S, D> {
block: SpeculativeDraftBlock<S, D>,
assumed_prefix: Vec<u32>,
}
impl<S, D> SpeculativeOptimisticBranch<S, D> {
pub fn new(block: SpeculativeDraftBlock<S, D>, assumed_prefix: Vec<u32>) -> Self {
Self {
block,
assumed_prefix,
}
}
}
#[non_exhaustive]
pub enum SpeculativeContinuation<S, D> {
None,
Promoted(SpeculativeDraftBlock<S, D>),
}
impl<S, D> SpeculativeContinuation<S, D> {
pub fn into_block(self) -> Option<SpeculativeDraftBlock<S, D>> {
match self {
Self::None => None,
Self::Promoted(block) => Some(block),
}
}
}
pub struct PendingSpeculativeVerification<E, D>
where
E: SpeculativeExecutor,
{
completion: E::Completion,
verification: E::Verification,
checkpoint: E::CacheCheckpoint,
block: SpeculativeDraftBlock<E::DraftState, D>,
optimistic: Option<SpeculativeOptimisticBranch<E::DraftState, D>>,
submitted: Instant,
submitted_tokens: usize,
}
impl<E, D> PendingSpeculativeVerification<E, D>
where
E: SpeculativeExecutor,
{
pub const fn block(&self) -> &SpeculativeDraftBlock<E::DraftState, D> {
&self.block
}
pub const fn has_optimistic_branch(&self) -> bool {
self.optimistic.is_some()
}
pub fn set_optimistic_branch(
&mut self,
branch: SpeculativeOptimisticBranch<E::DraftState, D>,
) -> Result<(), GenerationError> {
if self.optimistic.is_some() {
return Err(GenerationError::OptimisticBranchAlreadyPresent);
}
self.optimistic = Some(branch);
Ok(())
}
pub const fn submitted_tokens(&self) -> usize {
self.submitted_tokens
}
pub fn elapsed(&self) -> Duration {
self.submitted.elapsed()
}
}
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum SpeculativeOutputError {
#[error("speculative semantic state failed during {operation}: {message}")]
Semantic {
operation: String,
message: String,
},
#[error("speculative output publication failed: {message}")]
Publication {
message: String,
},
}
impl SpeculativeOutputError {
pub fn semantic(operation: impl Into<String>, message: impl Into<String>) -> Self {
Self::Semantic {
operation: operation.into(),
message: message.into(),
}
}
pub fn publication(message: impl Into<String>) -> Self {
Self::Publication {
message: message.into(),
}
}
}
pub trait SpeculativeConstraint: Sized {
fn fork(&self) -> Result<Self, SpeculativeOutputError>;
fn push_token(&mut self, token: u32) -> Result<bool, SpeculativeOutputError>;
fn finish(&mut self, reason: FinishReason) -> Result<(), SpeculativeOutputError>;
}
pub trait SpeculativePublisher<C> {
fn publish_committed(
&mut self,
constraint: &mut C,
tokens: &[u32],
cancellation: &GenerationCancellationToken,
sequence_finished: bool,
) -> Result<bool, SpeculativeOutputError>;
fn publish_cancelled(&mut self, constraint: &mut C) -> Result<(), SpeculativeOutputError>;
}
pub trait SpeculativeSemanticState {
fn fork_box(&self) -> Result<Box<dyn SpeculativeSemanticState>, SpeculativeOutputError>;
fn push_token(&mut self, token: u32) -> Result<bool, SpeculativeOutputError>;
fn finish(&mut self, reason: FinishReason) -> Result<(), SpeculativeOutputError>;
fn cancel(&mut self) -> Result<(), SpeculativeOutputError>;
fn take_events(&mut self) -> Vec<crate::generation::SemanticEvent>;
}
pub struct SpeculativeSemanticConstraint {
state: Option<Box<dyn SpeculativeSemanticState>>,
}
impl SpeculativeSemanticConstraint {
pub const fn plain() -> Self {
Self { state: None }
}
pub fn semantic(state: Box<dyn SpeculativeSemanticState>) -> Self {
Self { state: Some(state) }
}
}
impl SpeculativeConstraint for SpeculativeSemanticConstraint {
fn fork(&self) -> Result<Self, SpeculativeOutputError> {
Ok(Self {
state: self
.state
.as_ref()
.map(|state| state.fork_box())
.transpose()?,
})
}
fn push_token(&mut self, token: u32) -> Result<bool, SpeculativeOutputError> {
self.state
.as_mut()
.map(|state| state.push_token(token))
.transpose()
.map(|matched| matched.unwrap_or(false))
}
fn finish(&mut self, reason: FinishReason) -> Result<(), SpeculativeOutputError> {
if let Some(state) = &mut self.state {
state.finish(reason)?;
}
Ok(())
}
}
pub struct SpeculativeCallbackPublisher<'a> {
on_token: Box<dyn FnMut(u32) -> Result<(), SpeculativeOutputError> + 'a>,
on_event: Option<Box<dyn FnMut(crate::generation::SemanticEvent) + 'a>>,
}
impl<'a> SpeculativeCallbackPublisher<'a> {
pub fn tokens(on_token: impl FnMut(u32) -> Result<(), SpeculativeOutputError> + 'a) -> Self {
Self {
on_token: Box::new(on_token),
on_event: None,
}
}
pub fn semantic(on_event: impl FnMut(crate::generation::SemanticEvent) + 'a) -> Self {
Self {
on_token: Box::new(|_| Ok(())),
on_event: Some(Box::new(on_event)),
}
}
}
impl SpeculativePublisher<SpeculativeSemanticConstraint> for SpeculativeCallbackPublisher<'_> {
fn publish_committed(
&mut self,
constraint: &mut SpeculativeSemanticConstraint,
tokens: &[u32],
cancellation: &GenerationCancellationToken,
sequence_finished: bool,
) -> Result<bool, SpeculativeOutputError> {
for &token in tokens {
(self.on_token)(token)?;
}
let mut cancellation_won = false;
if let (Some(state), Some(on_event)) = (&mut constraint.state, &mut self.on_event) {
for event in state.take_events() {
on_event(event);
if cancellation.is_cancelled() && !sequence_finished {
cancellation_won = true;
break;
}
}
}
Ok(cancellation_won || (cancellation.is_cancelled() && !sequence_finished))
}
fn publish_cancelled(
&mut self,
constraint: &mut SpeculativeSemanticConstraint,
) -> Result<(), SpeculativeOutputError> {
if let (Some(state), Some(on_event)) = (&mut constraint.state, &mut self.on_event) {
state.cancel()?;
for event in state.take_events() {
on_event(event);
}
}
Ok(())
}
}
pub struct SpeculativeOutputRuntime<S, C, P> {
sampler: S,
sequence: GenerationSequence,
constraint: C,
publisher: P,
cancellation: GenerationCancellationToken,
}
impl<S, C, P> SpeculativeOutputRuntime<S, C, P>
where
S: SpeculativeSampling,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
pub fn new(
sampler: S,
sequence: GenerationSequence,
constraint: C,
publisher: P,
cancellation: GenerationCancellationToken,
) -> Self {
Self {
sampler,
sequence,
constraint,
publisher,
cancellation,
}
}
pub const fn sampler(&self) -> &S {
&self.sampler
}
pub const fn sampler_mut(&mut self) -> &mut S {
&mut self.sampler
}
pub const fn sequence(&self) -> &GenerationSequence {
&self.sequence
}
pub const fn sequence_mut(&mut self) -> &mut GenerationSequence {
&mut self.sequence
}
pub const fn constraint(&self) -> &C {
&self.constraint
}
pub const fn constraint_mut(&mut self) -> &mut C {
&mut self.constraint
}
pub const fn cancellation(&self) -> &GenerationCancellationToken {
&self.cancellation
}
pub fn cancel(&mut self) -> Result<(), SpeculativeOutputError> {
if self.sequence.cancel() {
self.publisher.publish_cancelled(&mut self.constraint)?;
}
Ok(())
}
pub fn install_committed_state(
&mut self,
sampler: S,
constraint: C,
sequence: GenerationSequence,
) {
self.sampler = sampler;
self.constraint = constraint;
self.sequence = sequence;
}
pub fn publish_committed(&mut self, tokens: &[u32]) -> Result<bool, SpeculativeOutputError> {
let cancellation_won = self.publisher.publish_committed(
&mut self.constraint,
tokens,
&self.cancellation,
self.sequence.is_finished(),
)? || (self.cancellation.is_cancelled()
&& !self.sequence.is_finished());
if cancellation_won {
self.cancel()?;
}
Ok(cancellation_won)
}
pub(crate) fn into_parts(self) -> (S, GenerationSequence, C, P) {
(self.sampler, self.sequence, self.constraint, self.publisher)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SpeculativeDriverError<E: std::error::Error + 'static> {
#[error(transparent)]
Backend(#[from] E),
#[error(transparent)]
Output(SpeculativeOutputError),
#[error(transparent)]
Generation(GenerationError),
}
pub struct ResolvedSpeculativeRound<S, C, R> {
sampler: S,
constraint: C,
sequence: GenerationSequence,
target_randomness: Option<R>,
accepted_proposals: usize,
committed_tokens: Vec<u32>,
verified_inputs: usize,
bonus_token: Option<u32>,
finish_reason: Option<FinishReason>,
}
#[allow(clippy::too_many_arguments)]
pub fn propose_block<'a, E, S>(
executor: &mut E,
sampler: &S,
state: &mut E::DraftState,
first_previous: u32,
count: usize,
base_history: &[u32],
temperature: f32,
eos_token_ids: &[u32],
draft_randomness: Option<&S::DraftRandomness>,
context: E::Context<'a>,
) -> Result<Vec<SpeculativeProposal<S::Distribution>>, SpeculativeDriverError<E::Error>>
where
E: SpeculativeExecutor + 'a,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error, Context<'a> = E::Context<'a>> + 'a,
{
let mut branch_sampler = sampler.clone();
let mut history = Vec::with_capacity(base_history.len() + count);
history.extend_from_slice(base_history);
let mut proposals: Vec<SpeculativeProposal<S::Distribution>> = Vec::with_capacity(count);
for offset in 0..count {
let previous = proposals
.last()
.map_or(first_previous, |proposal| proposal.token);
let raw = executor.proposal_logits(state, previous, context)?;
let distribution = branch_sampler.process_logits(
&raw,
temperature,
&history,
SamplingPlacement::Draft,
context,
)?;
let mut position_state = draft_randomness
.map(|root| S::draft_randomness_at(root, base_history.len() + offset, context))
.transpose()?;
let token = branch_sampler.sample(
&distribution,
temperature,
position_state.as_mut(),
SamplingPlacement::Draft,
context,
)?;
proposals.push(SpeculativeProposal {
token,
distribution,
});
history.push(token);
if eos_token_ids.contains(&token) || branch_sampler.prefix_is_complete(&history)? {
break;
}
}
Ok(proposals)
}
#[allow(clippy::too_many_arguments)]
pub fn resolve_round<'a, E, S, C>(
verification: &E::Verification,
mut proposals: Vec<SpeculativeProposal<S::Distribution>>,
sampler: &S,
sequence: &GenerationSequence,
constraint: &C,
target_randomness: Option<&S::RandomState>,
temperature: f32,
context: E::Context<'a>,
) -> Result<ResolvedSpeculativeRound<S, C, S::RandomState>, SpeculativeDriverError<E::Error>>
where
E: SpeculativeExecutor + 'a,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error, Context<'a> = E::Context<'a>> + 'a,
C: SpeculativeConstraint,
{
let mut draft_distributions = proposals
.iter_mut()
.map(|proposal| &mut proposal.distribution)
.collect::<Vec<_>>();
sampler.prepare_verification(&mut draft_distributions, temperature, context)?;
let proposal_count = proposals.len();
let mut sampler = sampler.clone();
let mut sequence = sequence.clone();
let mut constraint = constraint.fork().map_err(SpeculativeDriverError::Output)?;
let mut target_randomness = target_randomness.cloned();
let mut history = sequence.tokens().to_vec();
let mut round =
SpeculativeRound::new(proposal_count).map_err(SpeculativeDriverError::Generation)?;
let mut finish_reason = None;
for (index, proposal) in proposals.iter().enumerate() {
let raw = E::verification_logits(verification, index, context)?;
let target = sampler.process_logits(
&raw,
temperature,
&history,
SamplingPlacement::Target,
context,
)?;
match sampler.decide_proposal(
&target,
&proposal.distribution,
proposal.token,
temperature,
target_randomness.as_mut(),
context,
)? {
ProposalDecision::Accept => {
sampler.commit_token(
&target,
proposal.token,
SamplingPlacement::Target,
context,
)?;
history.push(proposal.token);
finish_reason = commit_terminal_token(
&mut sequence,
&mut sampler,
&mut constraint,
proposal.token,
)?;
round
.accept(proposal.token, finish_reason.is_some())
.map_err(SpeculativeDriverError::Generation)?;
if finish_reason.is_some() {
break;
}
}
ProposalDecision::Reject(replacement) => {
sampler.commit_token(&target, replacement, SamplingPlacement::Target, context)?;
finish_reason = commit_terminal_token(
&mut sequence,
&mut sampler,
&mut constraint,
replacement,
)?;
round
.reject_with(replacement, finish_reason.is_some())
.map_err(SpeculativeDriverError::Generation)?;
break;
}
}
}
let mut bonus_token = None;
if round.is_full_acceptance() && !sequence.is_finished() {
let raw = E::verification_logits(verification, proposal_count, context)?;
let target = sampler.process_logits(
&raw,
temperature,
&history,
SamplingPlacement::Target,
context,
)?;
let chosen = sampler.sample(
&target,
temperature,
target_randomness.as_mut(),
SamplingPlacement::Target,
context,
)?;
sampler.commit_token(&target, chosen, SamplingPlacement::Target, context)?;
finish_reason =
commit_terminal_token(&mut sequence, &mut sampler, &mut constraint, chosen)?;
round
.bonus(chosen, finish_reason.is_some())
.map_err(SpeculativeDriverError::Generation)?;
bonus_token = Some(chosen);
}
let plan = round
.commit_plan()
.map_err(SpeculativeDriverError::Generation)?;
Ok(ResolvedSpeculativeRound {
sampler,
constraint,
sequence,
target_randomness,
accepted_proposals: plan.accepted_proposals,
committed_tokens: plan.committed_tokens.to_vec(),
verified_inputs: plan.verified_inputs,
bonus_token,
finish_reason,
})
}
pub fn submit_verification_transaction<'a, E, D>(
executor: &mut E,
cache: &mut E::Cache,
last_committed_token: u32,
block: SpeculativeDraftBlock<E::DraftState, D>,
context: E::Context<'a>,
) -> Result<PendingSpeculativeVerification<E, D>, SpeculativeDriverError<E::Error>>
where
E: SpeculativeExecutor + 'a,
{
if block.proposals.is_empty() {
return Err(SpeculativeDriverError::Generation(
GenerationError::EmptyProposalBlock,
));
}
let mut input_tokens = Vec::with_capacity(block.proposals.len() + 1);
input_tokens.push(last_committed_token);
input_tokens.extend(block.proposals.iter().map(|proposal| proposal.token));
let checkpoint = E::checkpoint(cache);
let submission = executor.submit_verification(&input_tokens, cache, context)?;
Ok(PendingSpeculativeVerification {
completion: submission.completion,
verification: submission.output,
checkpoint,
block,
optimistic: None,
submitted: Instant::now(),
submitted_tokens: input_tokens.len(),
})
}
#[non_exhaustive]
pub enum SpeculativePublicationStatus<S, D> {
Continue(SpeculativeContinuation<S, D>),
Completed,
Cancelled,
}
pub struct PublishedSpeculativeVerification<TargetState, DraftState, Distribution, RandomState, T> {
target_state: TargetState,
target_randomness: Option<RandomState>,
stats: SpeculativeStats,
telemetry: T,
status: SpeculativePublicationStatus<DraftState, Distribution>,
}
pub type PublishedSpeculativeResult<E, S> = Result<
PublishedSpeculativeVerification<
<E as SpeculativeExecutor>::TargetState,
<E as SpeculativeExecutor>::DraftState,
<S as SpeculativeSampling>::Distribution,
<S as SpeculativeSampling>::RandomState,
<E as SpeculativeExecutor>::Telemetry,
>,
SpeculativeDriverError<<E as SpeculativeExecutor>::Error>,
>;
#[allow(clippy::too_many_arguments)]
pub fn resolve_commit_and_publish<'a, E, S, C, P>(
executor: &mut E,
cache: &mut E::Cache,
pending: PendingSpeculativeVerification<E, S::Distribution>,
runtime: &mut SpeculativeOutputRuntime<S, C, P>,
target_randomness: Option<&S::RandomState>,
temperature: f32,
mut stats: SpeculativeStats,
options: SpeculativeSchedulerOptions,
context: E::Context<'a>,
) -> PublishedSpeculativeResult<E, S>
where
E: SpeculativeExecutor + 'a,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error, Context<'a> = E::Context<'a>> + 'a,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
let PendingSpeculativeVerification {
completion,
mut verification,
checkpoint,
block,
optimistic,
submitted,
submitted_tokens: _,
} = pending;
completion.wait()?;
let telemetry = executor.take_verification_telemetry(&mut verification)?;
stats.verification_in_flight_time += submitted.elapsed();
let mut canonical_proposal_prefix = runtime.sequence().tokens().to_vec();
canonical_proposal_prefix.extend(block.proposals.iter().map(|proposal| proposal.token));
let resolved = resolve_round::<E, S, C>(
&verification,
block.proposals,
runtime.sampler(),
runtime.sequence(),
runtime.constraint(),
target_randomness,
temperature,
context,
)?;
let accepted = resolved.accepted_proposals;
let committed_tokens = resolved.committed_tokens;
let terminal = resolved.finish_reason;
let mut continuation = resolve_optimistic_branch(
optimistic,
&canonical_proposal_prefix,
resolved.bonus_token,
terminal.is_some(),
&mut stats,
)
.map_err(SpeculativeDriverError::Generation)?;
stats.accepted_tokens += accepted;
stats.accept_lens.push(accepted);
stats.rounds += 1;
let commit = executor.commit_verification(
verification,
block.state,
cache,
checkpoint,
resolved.verified_inputs,
context,
)?;
stats.target_tokens += commit.replayed_tokens;
stats.emitted_tokens += committed_tokens.len();
let target_randomness = resolved.target_randomness;
runtime.install_committed_state(resolved.sampler, resolved.constraint, resolved.sequence);
let cancelled = runtime
.publish_committed(&committed_tokens)
.map_err(SpeculativeDriverError::Output)?;
let status = if cancelled {
discard_continuation(&mut stats, continuation);
SpeculativePublicationStatus::Cancelled
} else if terminal.is_some() {
discard_continuation(&mut stats, continuation);
SpeculativePublicationStatus::Completed
} else {
stats.update_adaptive_lookahead(options);
SpeculativePublicationStatus::Continue(std::mem::replace(
&mut continuation,
SpeculativeContinuation::None,
))
};
Ok(PublishedSpeculativeVerification {
target_state: commit.state,
target_randomness,
stats,
telemetry,
status,
})
}
#[allow(clippy::too_many_arguments)]
pub fn cancel_pending_verification<'a, E, S, C, P>(
executor: &mut E,
cache: &mut E::Cache,
pending: PendingSpeculativeVerification<E, S::Distribution>,
runtime: &mut SpeculativeOutputRuntime<S, C, P>,
mut stats: SpeculativeStats,
context: E::Context<'a>,
) -> Result<(SpeculativeStats, E::Telemetry), SpeculativeDriverError<E::Error>>
where
E: SpeculativeExecutor + 'a,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error, Context<'a> = E::Context<'a>> + 'a,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
let PendingSpeculativeVerification {
completion,
mut verification,
checkpoint,
block,
optimistic,
submitted,
submitted_tokens: _,
} = pending;
completion.wait()?;
let telemetry = executor.take_verification_telemetry(&mut verification)?;
stats.verification_in_flight_time += submitted.elapsed();
discard_branch(&mut stats, optimistic);
let commit =
executor.commit_verification(verification, block.state, cache, checkpoint, 1, context)?;
stats.target_tokens += commit.replayed_tokens;
runtime.cancel().map_err(SpeculativeDriverError::Output)?;
Ok((stats, telemetry))
}
pub fn resolve_optimistic_branch<S, D>(
branch: Option<SpeculativeOptimisticBranch<S, D>>,
canonical_prefix: &[u32],
bonus: Option<u32>,
terminal: bool,
stats: &mut SpeculativeStats,
) -> Result<SpeculativeContinuation<S, D>, GenerationError> {
let Some(branch) = branch else {
return Ok(SpeculativeContinuation::None);
};
let Some(bonus) = bonus else {
discard_branch(stats, Some(branch));
return Ok(SpeculativeContinuation::None);
};
let optimistic_tokens = branch
.block
.proposals
.iter()
.map(|proposal| proposal.token)
.collect::<Vec<_>>();
let decision = crate::generation::resolve_optimistic_reuse(
&branch.assumed_prefix,
canonical_prefix,
&optimistic_tokens,
bonus,
terminal,
)?;
stats.optimistic_target_bonus_tokens += 1;
if decision == crate::generation::OptimisticReuseDecision::DiscardTerminal {
discard_branch(stats, Some(branch));
return Ok(SpeculativeContinuation::None);
}
let drafted = branch.block.proposals.len();
let SpeculativeDraftBlock { state, proposals } = branch.block;
let mut proposals = proposals.into_iter();
let _matched_or_discarded = proposals
.next()
.expect("validated optimistic branch is non-empty");
Ok(match decision {
crate::generation::OptimisticReuseDecision::DiscardMismatch => {
stats.optimistic_bonus_mismatches += 1;
stats.discarded_optimistic_tokens += drafted;
stats.discarded_optimistic_blocks += 1;
SpeculativeContinuation::None
}
crate::generation::OptimisticReuseDecision::MatchedConsumed => {
stats.optimistic_bonus_matches += 1;
stats.consumed_optimistic_tokens += 1;
SpeculativeContinuation::None
}
crate::generation::OptimisticReuseDecision::MatchedRetained => {
stats.optimistic_bonus_matches += 1;
stats.consumed_optimistic_tokens += 1;
let proposals = proposals.collect::<Vec<_>>();
stats.draft_tokens += proposals.len();
stats.reused_optimistic_tokens += proposals.len();
stats.reused_optimistic_blocks += 1;
SpeculativeContinuation::Promoted(SpeculativeDraftBlock { state, proposals })
}
crate::generation::OptimisticReuseDecision::DiscardTerminal => {
unreachable!("terminal decision handled before branch destruction")
}
})
}
fn discard_branch<S, D>(
stats: &mut SpeculativeStats,
branch: Option<SpeculativeOptimisticBranch<S, D>>,
) {
if let Some(branch) = branch {
stats.discarded_optimistic_tokens += branch.block.proposals.len();
stats.discarded_optimistic_blocks += 1;
}
}
fn discard_continuation<S, D>(
stats: &mut SpeculativeStats,
continuation: SpeculativeContinuation<S, D>,
) {
if let SpeculativeContinuation::Promoted(block) = continuation {
stats.discarded_optimistic_tokens += block.proposals.len();
stats.discarded_optimistic_blocks += 1;
stats.draft_tokens = stats.draft_tokens.saturating_sub(block.proposals.len());
stats.reused_optimistic_tokens = stats
.reused_optimistic_tokens
.saturating_sub(block.proposals.len());
stats.reused_optimistic_blocks = stats.reused_optimistic_blocks.saturating_sub(1);
}
}
fn commit_terminal_token<S, C>(
sequence: &mut GenerationSequence,
sampler: &mut S,
constraint: &mut C,
token: u32,
) -> Result<Option<FinishReason>, SpeculativeDriverError<S::Error>>
where
S: SpeculativeSampling,
C: SpeculativeConstraint,
{
let stop_matched = constraint
.push_token(token)
.map_err(SpeculativeDriverError::Output)?;
let grammar_complete = if stop_matched {
false
} else {
sampler.grammar_is_complete()?
};
let reason = sequence
.commit(
token,
TokenTerminalSignals {
stop_sequence: stop_matched,
grammar_complete,
},
)
.map_err(SpeculativeDriverError::Generation)?
.finish_reason;
if let Some(reason) = reason {
constraint
.finish(reason)
.map_err(SpeculativeDriverError::Output)?;
}
Ok(reason)
}
pub struct SpeculativeRequest<'cache, E, S, C, P>
where
E: SpeculativeExecutor,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error>,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
id: SpeculativeRequestId,
cache: &'cache mut E::Cache,
config: SpeculativeConfig,
runtime: SpeculativeOutputRuntime<S, C, P>,
target_randomness: Option<S::RandomState>,
draft_randomness: Option<S::DraftRandomness>,
stats: SpeculativeStats,
started: Instant,
target_state: Option<E::TargetState>,
block: Option<SpeculativeDraftBlock<E::DraftState, S::Distribution>>,
pending: Option<PendingSpeculativeVerification<E, S::Distribution>>,
lifecycle: SpeculativeRequestLifecycle,
}
impl<'cache, E, S, C, P> SpeculativeRequest<'cache, E, S, C, P>
where
E: SpeculativeExecutor,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error>,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
pub const fn id(&self) -> SpeculativeRequestId {
self.id
}
pub const fn status(&self) -> SpeculativeRequestStatus {
self.lifecycle.status()
}
pub const fn stats(&self) -> &SpeculativeStats {
&self.stats
}
pub const fn sequence(&self) -> &GenerationSequence {
self.runtime.sequence()
}
pub const fn sampler(&self) -> &S {
self.runtime.sampler()
}
pub const fn block(&self) -> Option<&SpeculativeDraftBlock<E::DraftState, S::Distribution>> {
self.block.as_ref()
}
pub const fn has_pending_verification(&self) -> bool {
self.pending.is_some()
}
fn transition(
&mut self,
next: SpeculativeRequestStatus,
) -> Result<(), SpeculativeDriverError<E::Error>> {
self.lifecycle
.transition(next)
.map_err(SpeculativeDriverError::Generation)
}
fn request_cancellation(&mut self) -> Result<(), SpeculativeDriverError<E::Error>> {
match self
.lifecycle
.request_cancellation(self.pending.is_some())
.map_err(SpeculativeDriverError::Generation)?
{
SpeculativeCancellationDisposition::AlreadyTerminal
| SpeculativeCancellationDisposition::Deferred => {}
SpeculativeCancellationDisposition::CancelNow => {
self.block = None;
self.runtime
.cancel()
.map_err(SpeculativeDriverError::Output)?;
self.stats.elapsed = self.started.elapsed();
}
}
Ok(())
}
fn candidate<'context>(
&self,
executor: &E,
optimistic_execution_available: bool,
) -> Result<SpeculativeCandidate, SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
let optimistic_eligible = if self.lifecycle.status()
!= SpeculativeRequestStatus::TargetVerificationInFlight
|| !optimistic_execution_available
{
false
} else {
let pending = self
.pending
.as_ref()
.expect("in-flight request retains its verification transaction");
let block = pending.block();
let assumed_len = self.runtime.sequence().tokens().len() + block.proposals.len();
let mut assumed_prefix = Vec::with_capacity(assumed_len);
assumed_prefix.extend_from_slice(self.runtime.sequence().tokens());
assumed_prefix.extend(block.proposals.iter().map(|proposal| proposal.token));
executor.supports_exact_optimistic_promotion()
&& self.runtime.sampler().supports_exact_optimistic_promotion()
&& !self.stats.adaptive_lookahead_disabled
&& !block.proposals.is_empty()
&& !self.runtime.sampler().prefix_is_complete(&assumed_prefix)?
&& !block
.proposals
.last()
.is_some_and(|proposal| self.config.eos_token_ids.contains(&proposal.token))
&& self.config.max_tokens.saturating_sub(assumed_len) > 1
};
Ok(SpeculativeCandidate {
status: self.lifecycle.status(),
optimistic_eligible,
})
}
fn draft_committed<'context>(
&mut self,
executor: &mut E,
context: E::Context<'context>,
) -> Result<bool, SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
let target_count = self
.config
.max_draft_tokens
.min(executor.max_proposals())
.min(
self.config
.max_tokens
.saturating_sub(self.runtime.sequence().tokens().len()),
);
if target_count == 0 {
self.transition(SpeculativeRequestStatus::Completed)?;
self.stats.elapsed = self.started.elapsed();
return Ok(false);
}
let mut block = if let Some(block) = self.block.take() {
block
} else {
let last = *self
.runtime
.sequence()
.tokens()
.last()
.expect("prefill emitted a token");
let target_state = self
.target_state
.as_ref()
.expect("ready request has target state");
SpeculativeDraftBlock {
state: executor.begin_proposal(target_state, last, target_count, context)?,
proposals: Vec::new(),
}
};
if block.proposals.len() > target_count {
return Err(SpeculativeDriverError::Generation(
GenerationError::ProposalCapacityExceeded {
proposed: block.proposals.len(),
capacity: target_count,
},
));
}
let additional = if block
.proposals
.last()
.is_some_and(|proposal| self.config.eos_token_ids.contains(&proposal.token))
{
0
} else {
target_count - block.proposals.len()
};
if additional > 0 {
let mut history =
Vec::with_capacity(self.runtime.sequence().tokens().len() + block.proposals.len());
history.extend_from_slice(self.runtime.sequence().tokens());
history.extend(block.proposals.iter().map(|proposal| proposal.token));
let previous = block.proposals.last().map_or_else(
|| {
*self
.runtime
.sequence()
.tokens()
.last()
.expect("prefill emitted a token")
},
|proposal| proposal.token,
);
let proposals = propose_block(
executor,
self.runtime.sampler(),
&mut block.state,
previous,
additional,
&history,
self.config.temperature,
&self.config.eos_token_ids,
self.draft_randomness.as_ref(),
context,
)?;
self.stats.draft_tokens += proposals.len();
block.proposals.extend(proposals);
}
executor.take_telemetry()?.record(&mut self.stats);
self.block = Some(block);
self.transition(SpeculativeRequestStatus::ReadyToSubmitVerification)?;
Ok(additional > 0)
}
fn submit_verification<'context>(
&mut self,
executor: &mut E,
context: E::Context<'context>,
) -> Result<(), SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
let block = self
.block
.take()
.expect("verification-ready request has a draft block");
let last = *self
.runtime
.sequence()
.tokens()
.last()
.expect("prefill emitted a token");
let pending = submit_verification_transaction(executor, self.cache, last, block, context)?;
self.stats.target_tokens += pending.submitted_tokens();
self.pending = Some(pending);
self.transition(SpeculativeRequestStatus::TargetVerificationInFlight)
}
fn draft_optimistic<'context>(
&mut self,
executor: &mut E,
context: E::Context<'context>,
) -> Result<(), SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
let started = Instant::now();
self.transition(SpeculativeRequestStatus::OptimisticDraftRunning)?;
let pending = self
.pending
.as_mut()
.expect("optimistic request has an in-flight verification");
let block = pending.block();
let assumed_len = self.runtime.sequence().tokens().len() + block.proposals.len();
let count = self
.config
.max_draft_tokens
.min(executor.max_proposals())
.min(self.config.max_tokens.saturating_sub(assumed_len));
let mut state = block.state.clone();
let last = block
.proposals
.last()
.expect("optimistic block has an assumed token")
.token;
let mut history = Vec::with_capacity(assumed_len);
history.extend_from_slice(self.runtime.sequence().tokens());
history.extend(block.proposals.iter().map(|proposal| proposal.token));
let proposals = propose_block(
executor,
self.runtime.sampler(),
&mut state,
last,
count,
&history,
self.config.temperature,
&self.config.eos_token_ids,
self.draft_randomness.as_ref(),
context,
)?;
self.stats.optimistic_draft_tokens += proposals.len();
self.stats.optimistic_draft_blocks += 1;
self.stats.optimistic_draft_time += started.elapsed();
pending
.set_optimistic_branch(SpeculativeOptimisticBranch {
block: SpeculativeDraftBlock { state, proposals },
assumed_prefix: history,
})
.map_err(SpeculativeDriverError::Generation)?;
self.transition(SpeculativeRequestStatus::OptimisticDraftReady)
}
fn resolve_verification<'context>(
&mut self,
executor: &mut E,
options: SpeculativeSchedulerOptions,
context: E::Context<'context>,
) -> Result<(), SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
self.transition(SpeculativeRequestStatus::VerificationResolution)?;
let pending = self
.pending
.take()
.expect("resolving request has an in-flight verification");
if self.lifecycle.cancellation_pending() || self.runtime.cancellation().is_cancelled() {
let (mut stats, telemetry) = cancel_pending_verification(
executor,
self.cache,
pending,
&mut self.runtime,
self.stats.clone(),
context,
)?;
telemetry.record(&mut stats);
self.stats = stats;
self.transition(SpeculativeRequestStatus::Cancelled)?;
self.stats.elapsed = self.started.elapsed();
return Ok(());
}
let mut published = resolve_commit_and_publish(
executor,
self.cache,
pending,
&mut self.runtime,
self.target_randomness.as_ref(),
self.config.temperature,
self.stats.clone(),
options,
context,
)?;
published.telemetry.record(&mut published.stats);
self.target_state = Some(published.target_state);
self.target_randomness = published.target_randomness;
self.stats = published.stats;
match published.status {
SpeculativePublicationStatus::Continue(continuation) => {
self.block = continuation.into_block();
self.transition(SpeculativeRequestStatus::ReadyToDraft)?;
}
SpeculativePublicationStatus::Completed => {
self.transition(SpeculativeRequestStatus::Completed)?;
self.stats.elapsed = self.started.elapsed();
}
SpeculativePublicationStatus::Cancelled => {
self.transition(SpeculativeRequestStatus::Cancelled)?;
self.stats.elapsed = self.started.elapsed();
}
}
Ok(())
}
}
pub struct CompletedSpeculativeRequest<S> {
id: SpeculativeRequestId,
token_ids: Vec<u32>,
stats: SpeculativeStats,
sampler: S,
finish_reason: Option<FinishReason>,
status: SpeculativeRequestStatus,
}
impl<S> CompletedSpeculativeRequest<S> {
pub const fn id(&self) -> SpeculativeRequestId {
self.id
}
pub fn token_ids(&self) -> &[u32] {
&self.token_ids
}
pub const fn stats(&self) -> &SpeculativeStats {
&self.stats
}
pub const fn sampler(&self) -> &S {
&self.sampler
}
pub const fn finish_reason(&self) -> Option<FinishReason> {
self.finish_reason
}
pub const fn status(&self) -> SpeculativeRequestStatus {
self.status
}
pub fn into_artifact(self) -> CompletedSpeculativeRequestArtifact<S> {
CompletedSpeculativeRequestArtifact {
id: self.id,
token_ids: self.token_ids,
stats: self.stats,
sampler: self.sampler,
finish_reason: self.finish_reason,
status: self.status,
}
}
}
pub struct CompletedSpeculativeRequestArtifact<S> {
id: SpeculativeRequestId,
token_ids: Vec<u32>,
stats: SpeculativeStats,
sampler: S,
finish_reason: Option<FinishReason>,
status: SpeculativeRequestStatus,
}
impl<S> CompletedSpeculativeRequestArtifact<S> {
pub const fn id(&self) -> SpeculativeRequestId {
self.id
}
pub fn take_token_ids(&mut self) -> Vec<u32> {
std::mem::take(&mut self.token_ids)
}
pub fn take_stats(&mut self) -> SpeculativeStats {
std::mem::take(&mut self.stats)
}
pub fn into_sampler(self) -> S {
self.sampler
}
pub const fn finish_reason(&self) -> Option<FinishReason> {
self.finish_reason
}
pub const fn status(&self) -> SpeculativeRequestStatus {
self.status
}
}
pub struct CompletedSpeculativeSchedule<S> {
requests: Vec<CompletedSpeculativeRequest<S>>,
scheduler: SpeculativeSchedulerStats,
}
impl<S> CompletedSpeculativeSchedule<S> {
pub fn into_requests(self) -> Vec<CompletedSpeculativeRequest<S>> {
self.requests
}
pub fn take_requests(&mut self) -> Vec<CompletedSpeculativeRequest<S>> {
std::mem::take(&mut self.requests)
}
pub fn take_scheduler(&mut self) -> SpeculativeSchedulerStats {
std::mem::take(&mut self.scheduler)
}
pub const fn scheduler(&self) -> &SpeculativeSchedulerStats {
&self.scheduler
}
}
pub struct SpeculativeRequestTable<'cache, E, S, C, P>
where
E: SpeculativeExecutor,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error>,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
schedule: SpeculativeSchedule,
requests: Vec<SpeculativeRequest<'cache, E, S, C, P>>,
stats: SpeculativeSchedulerStats,
}
impl<'cache, E, S, C, P> SpeculativeRequestTable<'cache, E, S, C, P>
where
E: SpeculativeExecutor,
S: SpeculativeSampling<Logits = E::Logits, Error = E::Error>,
C: SpeculativeConstraint,
P: SpeculativePublisher<C>,
{
pub fn new(
options: SpeculativeSchedulerOptions,
topology: SpeculativeExecutionTopology,
) -> Result<Self, GenerationError> {
Ok(Self {
schedule: SpeculativeSchedule::new(options)?,
requests: Vec::new(),
stats: SpeculativeSchedulerStats {
execution_topology: topology,
..SpeculativeSchedulerStats::default()
},
})
}
pub fn request(
&self,
id: SpeculativeRequestId,
) -> Option<&SpeculativeRequest<'cache, E, S, C, P>> {
self.requests.get(id.index())
}
pub fn status(&self, id: SpeculativeRequestId) -> Option<SpeculativeRequestStatus> {
self.request(id).map(SpeculativeRequest::status)
}
pub fn is_finished(&self) -> bool {
self.requests
.iter()
.all(|request| request.lifecycle.is_terminal())
}
pub const fn options(&self) -> SpeculativeSchedulerOptions {
self.schedule.options()
}
#[allow(clippy::too_many_arguments)]
pub fn submit<'context>(
&mut self,
executor: &mut E,
cache: &'cache mut E::Cache,
input: E::Input,
config: SpeculativeConfig,
mut runtime: SpeculativeOutputRuntime<S, C, P>,
randomness: SpeculativeRandomness<S::RandomState, S::DraftRandomness>,
component_timings_collected: bool,
context: E::Context<'context>,
) -> Result<SpeculativeRequestId, SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
config
.validate()
.map_err(SpeculativeDriverError::Generation)?;
if executor.max_proposals() == 0 {
return Err(SpeculativeDriverError::Generation(
GenerationError::NoBackendDraftCapacity,
));
}
let id = SpeculativeRequestId::new(self.requests.len());
let started = Instant::now();
let mut stats = SpeculativeStats {
execution_topology: self.stats.execution_topology,
component_timings_collected,
..SpeculativeStats::default()
};
let (target_randomness, draft_randomness) = (randomness.target, randomness.draft);
let (target_state, lifecycle) = if runtime.cancellation().is_cancelled() {
runtime.cancel().map_err(SpeculativeDriverError::Output)?;
stats.elapsed = started.elapsed();
(None, SpeculativeRequestLifecycle::cancelled())
} else if runtime.sequence().is_finished() {
stats.elapsed = started.elapsed();
(None, SpeculativeRequestLifecycle::completed())
} else {
let prefill = executor.prefill(input, cache, context)?;
stats.target_tokens = prefill.evaluated_tokens;
stats.scheduler_turns = 1;
let mut sampler = runtime.sampler().clone();
let mut constraint = runtime
.constraint()
.fork()
.map_err(SpeculativeDriverError::Output)?;
let mut sequence = runtime.sequence().clone();
let mut target_randomness = target_randomness.clone();
let first_logits = sampler.process_logits(
&prefill.logits,
config.temperature,
&[],
SamplingPlacement::Target,
context,
)?;
let first = sampler.sample(
&first_logits,
config.temperature,
target_randomness.as_mut(),
SamplingPlacement::Target,
context,
)?;
sampler.commit_token(&first_logits, first, SamplingPlacement::Target, context)?;
let reason =
commit_terminal_token(&mut sequence, &mut sampler, &mut constraint, first)?;
runtime.install_committed_state(sampler, constraint, sequence);
let cancelled = runtime
.publish_committed(&[first])
.map_err(SpeculativeDriverError::Output)?;
stats.emitted_tokens = 1;
let lifecycle = if cancelled {
stats.elapsed = started.elapsed();
SpeculativeRequestLifecycle::cancelled()
} else if reason.is_some() {
stats.elapsed = started.elapsed();
SpeculativeRequestLifecycle::completed()
} else {
let mut lifecycle = SpeculativeRequestLifecycle::new();
lifecycle
.transition(SpeculativeRequestStatus::ReadyToDraft)
.map_err(SpeculativeDriverError::Generation)?;
lifecycle
};
self.stats.turns += 1;
self.requests.push(SpeculativeRequest {
id,
cache,
config,
runtime,
target_randomness,
draft_randomness,
stats,
started,
target_state: Some(prefill.state),
block: None,
pending: None,
lifecycle,
});
return Ok(id);
};
self.requests.push(SpeculativeRequest {
id,
cache,
config,
runtime,
target_randomness,
draft_randomness,
stats,
started,
target_state,
block: None,
pending: None,
lifecycle,
});
Ok(id)
}
pub fn cancel(
&mut self,
id: SpeculativeRequestId,
) -> Result<(), SpeculativeDriverError<E::Error>> {
let request = self.requests.get_mut(id.index()).ok_or_else(|| {
SpeculativeDriverError::Generation(GenerationError::UnknownSpeculativeRequest {
index: id.index(),
})
})?;
request.request_cancellation()
}
pub fn step<'context>(
&mut self,
executor: &mut E,
optimistic_execution_available: bool,
context: E::Context<'context>,
) -> Result<bool, SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
let cancelled = self
.requests
.iter()
.filter(|request| {
request.runtime.cancellation().is_cancelled() && !request.lifecycle.is_terminal()
})
.map(|request| request.id)
.collect::<Vec<_>>();
for id in cancelled {
self.cancel(id)?;
}
if self.is_finished() {
return Ok(false);
}
let candidates = self
.requests
.iter()
.map(|request| request.candidate(executor, optimistic_execution_available))
.collect::<Result<Vec<_>, _>>()?;
let Some(action) = self
.schedule
.next_action(&candidates)
.map_err(SpeculativeDriverError::Generation)?
else {
return Ok(false);
};
let index = match action {
SpeculativeAction::SubmitVerification(index)
| SpeculativeAction::DraftOptimistic(index)
| SpeculativeAction::ResolveVerification(index)
| SpeculativeAction::DraftCommitted { index, .. } => index,
};
self.stats.turns += 1;
self.requests[index].stats.scheduler_turns += 1;
match action {
SpeculativeAction::SubmitVerification(index) => {
self.requests[index].submit_verification(executor, context)?;
let in_flight = self
.requests
.iter()
.filter(|request| request.pending.is_some())
.count();
self.stats.peak_in_flight_verifications =
self.stats.peak_in_flight_verifications.max(in_flight);
}
SpeculativeAction::DraftCommitted {
index,
cross_request,
} => {
let drafted = self.requests[index].draft_committed(executor, context)?;
if cross_request && drafted {
self.requests[index].stats.cross_request_draft_opportunities += 1;
self.stats.cross_request_draft_opportunities += 1;
}
}
SpeculativeAction::DraftOptimistic(index) => {
self.requests[index].draft_optimistic(executor, context)?;
let optimistic = self
.requests
.iter()
.filter(|request| {
request
.pending
.as_ref()
.is_some_and(PendingSpeculativeVerification::has_optimistic_branch)
})
.count();
self.stats.peak_optimistic_branches =
self.stats.peak_optimistic_branches.max(optimistic);
}
SpeculativeAction::ResolveVerification(index) => {
self.requests[index].resolve_verification(
executor,
self.schedule.options(),
context,
)?;
}
}
Ok(true)
}
pub fn run<'context>(
&mut self,
executor: &mut E,
optimistic_execution_available: bool,
context: E::Context<'context>,
) -> Result<(), SpeculativeDriverError<E::Error>>
where
E: 'context,
S: SpeculativeSampling<
Logits = E::Logits,
Error = E::Error,
Context<'context> = E::Context<'context>,
> + 'context,
{
while self.step(executor, optimistic_execution_available, context)? {}
Ok(())
}
pub fn finish(
self,
) -> Result<CompletedSpeculativeSchedule<S>, SpeculativeDriverError<E::Error>> {
if !self.is_finished() {
return Err(SpeculativeDriverError::Generation(
GenerationError::ActiveSpeculativeRequests,
));
}
Ok(CompletedSpeculativeSchedule {
requests: self
.requests
.into_iter()
.map(|request| {
let (sampler, sequence, _, _) = request.runtime.into_parts();
CompletedSpeculativeRequest {
id: request.id,
finish_reason: sequence.finish_reason(),
token_ids: sequence.into_tokens(),
stats: request.stats,
sampler,
status: request.lifecycle.status(),
}
})
.collect(),
scheduler: self.stats,
})
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct SpeculativeCandidate {
status: SpeculativeRequestStatus,
optimistic_eligible: bool,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum SpeculativeAction {
SubmitVerification(usize),
DraftCommitted {
index: usize,
cross_request: bool,
},
DraftOptimistic(usize),
ResolveVerification(usize),
}
pub struct SpeculativeSchedule {
options: SpeculativeSchedulerOptions,
cursor: usize,
}
impl SpeculativeSchedule {
pub fn new(options: SpeculativeSchedulerOptions) -> Result<Self, GenerationError> {
Ok(Self {
options: options.validate()?,
cursor: 0,
})
}
pub const fn options(&self) -> SpeculativeSchedulerOptions {
self.options
}
pub fn next_action(
&mut self,
candidates: &[SpeculativeCandidate],
) -> Result<Option<SpeculativeAction>, GenerationError> {
if candidates.iter().all(|candidate| {
matches!(
candidate.status,
SpeculativeRequestStatus::Completed | SpeculativeRequestStatus::Cancelled
)
}) {
return Ok(None);
}
let in_flight = candidates
.iter()
.filter(|candidate| {
matches!(
candidate.status,
SpeculativeRequestStatus::TargetVerificationInFlight
| SpeculativeRequestStatus::OptimisticDraftRunning
| SpeculativeRequestStatus::OptimisticDraftReady
| SpeculativeRequestStatus::VerificationResolution
)
})
.count();
let optimistic = candidates
.iter()
.filter(|candidate| candidate.status == SpeculativeRequestStatus::OptimisticDraftReady)
.count();
if in_flight < self.options.max_in_flight_verifications {
if let Some(index) = self.select(candidates, |candidate| {
candidate.status == SpeculativeRequestStatus::ReadyToSubmitVerification
}) {
return Ok(Some(SpeculativeAction::SubmitVerification(index)));
}
}
if in_flight > 0 {
if optimistic < self.options.max_optimistic_branches
&& self.options.lookahead_blocks > 0
{
if let Some(index) = self.select(candidates, |candidate| {
candidate.status == SpeculativeRequestStatus::TargetVerificationInFlight
&& candidate.optimistic_eligible
}) {
return Ok(Some(SpeculativeAction::DraftOptimistic(index)));
}
}
if let Some(index) = self.select(candidates, |candidate| {
candidate.status == SpeculativeRequestStatus::ReadyToDraft
}) {
return Ok(Some(SpeculativeAction::DraftCommitted {
index,
cross_request: true,
}));
}
if let Some(index) = self.select(candidates, |candidate| {
matches!(
candidate.status,
SpeculativeRequestStatus::TargetVerificationInFlight
| SpeculativeRequestStatus::OptimisticDraftReady
)
}) {
return Ok(Some(SpeculativeAction::ResolveVerification(index)));
}
} else if let Some(index) = self.select(candidates, |candidate| {
candidate.status == SpeculativeRequestStatus::ReadyToDraft
}) {
return Ok(Some(SpeculativeAction::DraftCommitted {
index,
cross_request: false,
}));
}
Err(GenerationError::StalledSpeculativeSchedule)
}
fn select(
&mut self,
candidates: &[SpeculativeCandidate],
predicate: impl Fn(&SpeculativeCandidate) -> bool,
) -> Option<usize> {
for offset in 0..candidates.len() {
let index = (self.cursor + offset) % candidates.len();
if predicate(&candidates[index]) {
self.cursor = (index + 1) % candidates.len();
return Some(index);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{cell::RefCell, convert::Infallible, rc::Rc};
type TransactionTrace = Rc<RefCell<Vec<&'static str>>>;
#[test]
fn speculative_capability_schema_round_trips_without_backend_identity() {
let capability = SpeculativeCapability::Unsupported {
draft_source: SpeculativeDraftSource::Embedded,
architecture: "future_decoder".into(),
};
let json = serde_json::to_string(&capability).unwrap();
assert_eq!(
serde_json::from_str::<SpeculativeCapability>(&json).unwrap(),
capability
);
assert!(!json.contains("mlx"));
}
#[derive(Debug, Clone, Default)]
struct Done {
trace: Option<TransactionTrace>,
}
impl Completion for Done {
type Error = Infallible;
fn is_complete(&self) -> Result<bool, Self::Error> {
Ok(true)
}
fn wait(&self) -> Result<(), Self::Error> {
if let Some(trace) = &self.trace {
trace.borrow_mut().push("wait");
}
Ok(())
}
}
#[derive(Clone, Default)]
struct PortableSemanticState {
events: Vec<crate::generation::SemanticEvent>,
}
impl SpeculativeSemanticState for PortableSemanticState {
fn fork_box(&self) -> Result<Box<dyn SpeculativeSemanticState>, SpeculativeOutputError> {
let mut fork = self.clone();
fork.events.clear();
Ok(Box::new(fork))
}
fn push_token(&mut self, token: u32) -> Result<bool, SpeculativeOutputError> {
self.events
.push(crate::generation::SemanticEvent::TextDelta(
token.to_string(),
));
Ok(false)
}
fn finish(&mut self, reason: FinishReason) -> Result<(), SpeculativeOutputError> {
self.events
.push(crate::generation::SemanticEvent::Finished { reason });
Ok(())
}
fn cancel(&mut self) -> Result<(), SpeculativeOutputError> {
self.finish(FinishReason::Cancelled)
}
fn take_events(&mut self) -> Vec<crate::generation::SemanticEvent> {
std::mem::take(&mut self.events)
}
}
#[test]
fn core_semantic_publisher_commits_and_cancels_without_backend_errors() {
let published = Rc::new(RefCell::new(Vec::new()));
let mut constraint =
SpeculativeSemanticConstraint::semantic(Box::new(PortableSemanticState::default()));
constraint.push_token(7).unwrap();
constraint.finish(FinishReason::MaxTokens).unwrap();
{
let published = Rc::clone(&published);
let mut publisher = SpeculativeCallbackPublisher::semantic(move |event| {
published.borrow_mut().push(event)
});
assert!(!publisher
.publish_committed(
&mut constraint,
&[7],
&GenerationCancellationToken::new(),
true,
)
.unwrap());
}
assert_eq!(
*published.borrow(),
vec![
crate::generation::SemanticEvent::TextDelta("7".into()),
crate::generation::SemanticEvent::Finished {
reason: FinishReason::MaxTokens,
},
]
);
let cancelled = Rc::new(RefCell::new(Vec::new()));
let mut constraint =
SpeculativeSemanticConstraint::semantic(Box::new(PortableSemanticState::default()));
{
let cancelled = Rc::clone(&cancelled);
let mut publisher = SpeculativeCallbackPublisher::semantic(move |event| {
cancelled.borrow_mut().push(event)
});
publisher.publish_cancelled(&mut constraint).unwrap();
}
assert_eq!(
*cancelled.borrow(),
vec![crate::generation::SemanticEvent::Finished {
reason: FinishReason::Cancelled,
}]
);
let mut constraint = SpeculativeSemanticConstraint::plain();
let mut publisher = SpeculativeCallbackPublisher::tokens(|_| {
Err(SpeculativeOutputError::publication("consumer closed"))
});
assert_eq!(
publisher
.publish_committed(
&mut constraint,
&[11],
&GenerationCancellationToken::new(),
false,
)
.unwrap_err(),
SpeculativeOutputError::publication("consumer closed")
);
}
#[derive(Default)]
struct MockExecutor {
trace: Option<TransactionTrace>,
}
struct MockVerification {
tokens: Vec<u32>,
logits: Vec<Vec<f32>>,
}
impl SpeculativeExecutor for MockExecutor {
type Input = Vec<u32>;
type Cache = Vec<u32>;
type TargetState = usize;
type DraftState = Vec<u32>;
type CacheCheckpoint = usize;
type Verification = MockVerification;
type Logits = Vec<f32>;
type Context<'a> = ();
type Completion = Done;
type Telemetry = ();
type Error = Infallible;
fn supports_exact_optimistic_promotion(&self) -> bool {
true
}
fn prefill<'context>(
&mut self,
input: Self::Input,
cache: &mut Self::Cache,
_: Self::Context<'context>,
) -> Result<SpeculativePrefill<Self::TargetState, Self::Logits>, Self::Error>
where
Self: 'context,
{
cache.extend_from_slice(&input);
Ok(SpeculativePrefill {
logits: vec![0.0, 1.0],
state: cache.len(),
evaluated_tokens: input.len(),
})
}
fn begin_proposal<'a>(
&mut self,
_: &Self::TargetState,
last_token: u32,
_: usize,
_: Self::Context<'a>,
) -> Result<Self::DraftState, Self::Error> {
Ok(vec![last_token])
}
fn proposal_logits<'a>(
&mut self,
state: &mut Self::DraftState,
last_token: u32,
_: Self::Context<'a>,
) -> Result<Self::Logits, Self::Error> {
state.push(last_token + 1);
Ok(vec![0.0, 1.0])
}
fn checkpoint(cache: &Self::Cache) -> Self::CacheCheckpoint {
cache.len()
}
fn submit_verification<'a>(
&mut self,
input_tokens: &[u32],
cache: &mut Self::Cache,
_: Self::Context<'a>,
) -> Result<Submission<Self::Verification, Self::Completion>, Self::Error> {
cache.extend_from_slice(input_tokens);
Ok(Submission {
output: MockVerification {
tokens: input_tokens.to_vec(),
logits: vec![vec![0.0, 1.0], vec![1.0, 0.0], vec![0.0, 1.0]],
},
completion: Done {
trace: self.trace.clone(),
},
})
}
fn verification_logits<'a>(
output: &Self::Verification,
index: usize,
_: Self::Context<'a>,
) -> Result<Self::Logits, Self::Error>
where
Self: 'a,
{
Ok(output.logits[index].clone())
}
fn commit_verification<'a>(
&mut self,
output: Self::Verification,
draft_state: Self::DraftState,
cache: &mut Self::Cache,
checkpoint: Self::CacheCheckpoint,
verified_inputs: usize,
_: Self::Context<'a>,
) -> Result<SpeculativeCommit<Self::TargetState>, Self::Error> {
assert!(!output.tokens.is_empty());
if let Some(trace) = &self.trace {
trace.borrow_mut().push("commit");
}
cache.truncate(checkpoint + verified_inputs);
Ok(SpeculativeCommit {
state: draft_state.len(),
replayed_tokens: 0,
})
}
}
#[test]
fn mock_executor_prefill_propose_verify_and_commit_without_a_tensor_runtime() {
let mut executor = MockExecutor::default();
let mut cache = Vec::new();
let prefill = executor.prefill(vec![4, 5], &mut cache, ()).unwrap();
let mut draft = executor.begin_proposal(&prefill.state, 5, 2, ()).unwrap();
assert_eq!(
executor.proposal_logits(&mut draft, 5, ()).unwrap(),
[0.0, 1.0]
);
let checkpoint = MockExecutor::checkpoint(&cache);
let submission = executor
.submit_verification(&[5, 6], &mut cache, ())
.unwrap();
submission.completion.wait().unwrap();
let commit = executor
.commit_verification(submission.output, draft, &mut cache, checkpoint, 1, ())
.unwrap();
assert_eq!(cache, [4, 5, 5]);
assert_eq!(commit.replayed_tokens, 0);
}
#[test]
fn execution_topology_is_a_portable_schema() {
let topology = SpeculativeExecutionTopology::CrossDeviceSplit;
let encoded = serde_json::to_string(&topology).unwrap();
assert_eq!(encoded, "\"cross_device_split\"");
assert_eq!(
serde_json::from_str::<SpeculativeExecutionTopology>(&encoded).unwrap(),
topology
);
}
#[derive(Clone, Default)]
struct MockSampling {
committed: Vec<u32>,
}
impl SpeculativeSampling for MockSampling {
type Logits = Vec<f32>;
type Distribution = Vec<f32>;
type Seed = ();
type RandomState = usize;
type DraftRandomness = usize;
type Context<'a> = ();
type Error = Infallible;
fn supports_exact_optimistic_promotion(&self) -> bool {
true
}
fn initialize_randomness<'a>(
_: Option<Self::Seed>,
_: f32,
_: Self::Context<'a>,
) -> Result<SpeculativeRandomness<Self::RandomState, Self::DraftRandomness>, Self::Error>
where
Self: 'a,
{
Ok(SpeculativeRandomness {
target: Some(0),
draft: Some(0),
})
}
fn draft_randomness_at<'a>(
root: &Self::DraftRandomness,
position: usize,
_: Self::Context<'a>,
) -> Result<Self::RandomState, Self::Error>
where
Self: 'a,
{
Ok(root + position)
}
fn process_logits<'a>(
&mut self,
logits: &Self::Logits,
_: f32,
_: &[u32],
_: SamplingPlacement,
_: Self::Context<'a>,
) -> Result<Self::Distribution, Self::Error>
where
Self: 'a,
{
Ok(logits.clone())
}
fn sample<'a>(
&self,
distribution: &Self::Distribution,
_: f32,
randomness: Option<&mut Self::RandomState>,
_: SamplingPlacement,
_: Self::Context<'a>,
) -> Result<u32, Self::Error>
where
Self: 'a,
{
if let Some(randomness) = randomness {
*randomness += 1;
}
Ok(argmax(distribution))
}
fn decide_proposal<'a>(
&self,
target: &Self::Distribution,
_: &Self::Distribution,
proposed: u32,
_: f32,
randomness: Option<&mut Self::RandomState>,
_: Self::Context<'a>,
) -> Result<ProposalDecision, Self::Error>
where
Self: 'a,
{
if let Some(randomness) = randomness {
*randomness += 1;
}
let target = argmax(target);
Ok(if target == proposed {
ProposalDecision::Accept
} else {
ProposalDecision::Reject(target)
})
}
fn commit_token<'a>(
&mut self,
_: &Self::Distribution,
token: u32,
_: SamplingPlacement,
_: Self::Context<'a>,
) -> Result<(), Self::Error>
where
Self: 'a,
{
self.committed.push(token);
Ok(())
}
}
fn argmax(values: &[f32]) -> u32 {
values
.iter()
.enumerate()
.max_by(|(_, left), (_, right)| left.total_cmp(right))
.map(|(index, _)| index as u32)
.unwrap()
}
#[derive(Default)]
struct MockConstraint {
tokens: Vec<u32>,
finished: Option<FinishReason>,
}
impl SpeculativeConstraint for MockConstraint {
fn fork(&self) -> Result<Self, SpeculativeOutputError> {
Ok(Self {
tokens: self.tokens.clone(),
finished: self.finished,
})
}
fn push_token(&mut self, token: u32) -> Result<bool, SpeculativeOutputError> {
self.tokens.push(token);
Ok(false)
}
fn finish(&mut self, reason: FinishReason) -> Result<(), SpeculativeOutputError> {
self.finished = Some(reason);
Ok(())
}
}
#[derive(Default)]
struct MockPublisher {
tokens: Vec<u32>,
cancelled: bool,
trace: Option<TransactionTrace>,
}
impl SpeculativePublisher<MockConstraint> for MockPublisher {
fn publish_committed(
&mut self,
_: &mut MockConstraint,
tokens: &[u32],
_: &GenerationCancellationToken,
_: bool,
) -> Result<bool, SpeculativeOutputError> {
if let Some(trace) = &self.trace {
trace.borrow_mut().push("publish");
}
self.tokens.extend_from_slice(tokens);
Ok(false)
}
fn publish_cancelled(
&mut self,
_: &mut MockConstraint,
) -> Result<(), SpeculativeOutputError> {
if let Some(trace) = &self.trace {
trace.borrow_mut().push("cancel");
}
self.cancelled = true;
Ok(())
}
}
fn mock_output_runtime(
cancellation: GenerationCancellationToken,
trace: Option<TransactionTrace>,
) -> SpeculativeOutputRuntime<MockSampling, MockConstraint, MockPublisher> {
let mut sequence = GenerationSequence::new(8, []);
sequence.commit(5, TokenTerminalSignals::default()).unwrap();
SpeculativeOutputRuntime::new(
MockSampling::default(),
sequence,
MockConstraint::default(),
MockPublisher {
trace,
..MockPublisher::default()
},
cancellation,
)
}
fn empty_mock_runtime(
max_tokens: usize,
cancellation: GenerationCancellationToken,
) -> SpeculativeOutputRuntime<MockSampling, MockConstraint, MockPublisher> {
SpeculativeOutputRuntime::new(
MockSampling::default(),
GenerationSequence::new(max_tokens, []),
MockConstraint::default(),
MockPublisher::default(),
cancellation,
)
}
#[test]
fn portable_driver_proposes_and_resolves_acceptance_and_replacement() {
let mut executor = MockExecutor::default();
let sampler = MockSampling::default();
let mut draft = executor.begin_proposal(&2, 5, 2, ()).unwrap();
let proposals = propose_block(
&mut executor,
&sampler,
&mut draft,
5,
2,
&[5],
0.7,
&[],
Some(&0),
(),
)
.unwrap();
assert_eq!(
proposals
.iter()
.map(|proposal| proposal.token)
.collect::<Vec<_>>(),
[1, 1]
);
let mut cache = vec![4, 5];
let verification = executor
.submit_verification(&[5, 1, 1], &mut cache, ())
.unwrap()
.output;
let mut sequence = GenerationSequence::new(8, []);
sequence.commit(5, TokenTerminalSignals::default()).unwrap();
let resolved = resolve_round::<MockExecutor, MockSampling, MockConstraint>(
&verification,
proposals,
&sampler,
&sequence,
&MockConstraint::default(),
Some(&0),
0.7,
(),
)
.unwrap();
assert_eq!(resolved.accepted_proposals, 1);
assert_eq!(resolved.committed_tokens, [1, 0]);
assert_eq!(resolved.verified_inputs, 2);
assert_eq!(resolved.sampler.committed, [1, 0]);
assert_eq!(resolved.sequence.tokens(), [5, 1, 0]);
assert_eq!(resolved.constraint.tokens, [1, 0]);
assert_eq!(resolved.target_randomness, Some(2));
assert_eq!(resolved.finish_reason, None);
}
#[test]
fn portable_schedule_is_fair_and_respects_retained_capacity() {
let mut schedule =
SpeculativeSchedule::new(SpeculativeSchedulerOptions::default()).unwrap();
let ready = SpeculativeCandidate {
status: SpeculativeRequestStatus::ReadyToSubmitVerification,
optimistic_eligible: false,
};
assert_eq!(
schedule.next_action(&[ready, ready]).unwrap(),
Some(SpeculativeAction::SubmitVerification(0))
);
assert_eq!(
schedule.next_action(&[ready, ready]).unwrap(),
Some(SpeculativeAction::SubmitVerification(1))
);
let in_flight = SpeculativeCandidate {
status: SpeculativeRequestStatus::TargetVerificationInFlight,
optimistic_eligible: false,
};
let draft = SpeculativeCandidate {
status: SpeculativeRequestStatus::ReadyToDraft,
optimistic_eligible: false,
};
assert_eq!(
schedule.next_action(&[in_flight, ready, draft]).unwrap(),
Some(SpeculativeAction::DraftCommitted {
index: 2,
cross_request: true,
})
);
}
#[test]
fn request_table_owns_actions_resources_fairness_and_deferred_cancellation() {
let mut executor = MockExecutor::default();
let mut first_cache = Vec::new();
let mut second_cache = Vec::new();
let options = SpeculativeSchedulerOptions::default().with_lookahead(false);
let mut table =
SpeculativeRequestTable::new(options, SpeculativeExecutionTopology::Single).unwrap();
let config = SpeculativeConfig {
max_tokens: 3,
max_draft_tokens: 2,
temperature: 0.7,
eos_token_ids: Vec::new(),
};
let first_cancellation = GenerationCancellationToken::new();
let first = table
.submit(
&mut executor,
&mut first_cache,
vec![4],
config.clone(),
empty_mock_runtime(config.max_tokens, first_cancellation.clone()),
SpeculativeRandomness {
target: Some(0),
draft: Some(0),
},
false,
(),
)
.unwrap();
let second = table
.submit(
&mut executor,
&mut second_cache,
vec![8],
config.clone(),
empty_mock_runtime(config.max_tokens, GenerationCancellationToken::new()),
SpeculativeRandomness {
target: Some(0),
draft: Some(10),
},
false,
(),
)
.unwrap();
assert_eq!(
table.status(first),
Some(SpeculativeRequestStatus::ReadyToDraft)
);
assert_eq!(
table.status(second),
Some(SpeculativeRequestStatus::ReadyToDraft)
);
table.step(&mut executor, false, ()).unwrap();
table.step(&mut executor, false, ()).unwrap();
assert!(table.request(first).unwrap().has_pending_verification());
first_cancellation.cancel();
table.run(&mut executor, false, ()).unwrap();
let output = table.finish().unwrap();
assert_eq!(output.requests.len(), 2);
assert_eq!(output.requests[0].id, first);
assert_eq!(
output.requests[0].status,
SpeculativeRequestStatus::Cancelled
);
assert_eq!(output.requests[0].token_ids, [1]);
assert_eq!(output.requests[1].id, second);
assert_eq!(
output.requests[1].status,
SpeculativeRequestStatus::Completed
);
assert_eq!(output.requests[1].token_ids, [1, 1, 0]);
assert!(output.scheduler.cross_request_draft_opportunities > 0);
assert_eq!(first_cache, [4, 1]);
assert_eq!(second_cache, [8, 1, 1]);
}
#[test]
fn request_table_applies_optimistic_actions_without_backend_scheduler_state() {
let mut executor = MockExecutor::default();
let mut cache = Vec::new();
let config = SpeculativeConfig {
max_tokens: 5,
max_draft_tokens: 2,
temperature: 0.7,
eos_token_ids: Vec::new(),
};
let mut table = SpeculativeRequestTable::new(
SpeculativeSchedulerOptions::default(),
SpeculativeExecutionTopology::SameDeviceSplit,
)
.unwrap();
let id = table
.submit(
&mut executor,
&mut cache,
vec![4],
config.clone(),
empty_mock_runtime(config.max_tokens, GenerationCancellationToken::new()),
SpeculativeRandomness {
target: Some(0),
draft: Some(0),
},
false,
(),
)
.unwrap();
table.step(&mut executor, true, ()).unwrap();
table.step(&mut executor, true, ()).unwrap();
table.step(&mut executor, true, ()).unwrap();
assert_eq!(
table.status(id),
Some(SpeculativeRequestStatus::OptimisticDraftReady)
);
table.run(&mut executor, true, ()).unwrap();
let output = table.finish().unwrap();
assert_eq!(
output.requests[0].status,
SpeculativeRequestStatus::Completed
);
assert!(output.requests[0].stats.optimistic_draft_blocks > 0);
assert!(output.requests[0].stats.discarded_optimistic_blocks > 0);
assert_eq!(output.scheduler.peak_optimistic_branches, 1);
}
#[test]
fn coordinator_commits_before_publication_and_discards_mismatched_lookahead() {
let trace = TransactionTrace::default();
let mut executor = MockExecutor {
trace: Some(trace.clone()),
};
let mut cache = vec![4, 5];
let block = SpeculativeDraftBlock {
state: vec![5, 1, 1],
proposals: vec![
SpeculativeProposal {
token: 1,
distribution: vec![0.0, 1.0],
},
SpeculativeProposal {
token: 1,
distribution: vec![0.0, 1.0],
},
],
};
let mut pending =
submit_verification_transaction(&mut executor, &mut cache, 5, block, ()).unwrap();
pending
.set_optimistic_branch(SpeculativeOptimisticBranch {
block: SpeculativeDraftBlock {
state: vec![5, 1, 1, 2],
proposals: vec![SpeculativeProposal {
token: 2,
distribution: vec![0.0, 0.0, 1.0],
}],
},
assumed_prefix: vec![5, 1, 1],
})
.unwrap();
let mut runtime =
mock_output_runtime(GenerationCancellationToken::new(), Some(trace.clone()));
let published = resolve_commit_and_publish(
&mut executor,
&mut cache,
pending,
&mut runtime,
Some(&0),
0.7,
SpeculativeStats::default(),
SpeculativeSchedulerOptions::default(),
(),
)
.unwrap();
assert!(matches!(
published.status,
SpeculativePublicationStatus::Continue(SpeculativeContinuation::None)
));
assert_eq!(published.stats.accepted_tokens, 1);
assert_eq!(published.stats.discarded_optimistic_tokens, 1);
assert_eq!(cache, [4, 5, 5, 1]);
let (_, sequence, constraint, publisher) = runtime.into_parts();
assert_eq!(sequence.tokens(), [5, 1, 0]);
assert_eq!(constraint.tokens, [1, 0]);
assert_eq!(publisher.tokens, [1, 0]);
assert!(!publisher.cancelled);
assert_eq!(*trace.borrow(), ["wait", "commit", "publish"]);
}
#[test]
fn coordinator_cancels_only_after_retained_verification_is_safe() {
let trace = TransactionTrace::default();
let mut executor = MockExecutor {
trace: Some(trace.clone()),
};
let mut cache = vec![4, 5];
let block = SpeculativeDraftBlock {
state: vec![5, 1],
proposals: vec![SpeculativeProposal {
token: 1,
distribution: vec![0.0, 1.0],
}],
};
let mut pending =
submit_verification_transaction(&mut executor, &mut cache, 5, block, ()).unwrap();
pending
.set_optimistic_branch(SpeculativeOptimisticBranch {
block: SpeculativeDraftBlock {
state: vec![5, 1, 2],
proposals: vec![SpeculativeProposal {
token: 2,
distribution: vec![0.0, 0.0, 1.0],
}],
},
assumed_prefix: vec![5, 1],
})
.unwrap();
let cancellation = GenerationCancellationToken::new();
cancellation.cancel();
let mut runtime = mock_output_runtime(cancellation, Some(trace.clone()));
let (stats, ()) = cancel_pending_verification(
&mut executor,
&mut cache,
pending,
&mut runtime,
SpeculativeStats::default(),
(),
)
.unwrap();
assert_eq!(stats.discarded_optimistic_tokens, 1);
assert_eq!(cache, [4, 5, 5]);
let (_, sequence, _, publisher) = runtime.into_parts();
assert_eq!(sequence.finish_reason(), Some(FinishReason::Cancelled));
assert!(publisher.tokens.is_empty());
assert!(publisher.cancelled);
assert_eq!(*trace.borrow(), ["wait", "commit", "cancel"]);
}
}