use std::sync::Arc;
use ferrum_engine::{
speculative::SpeculativeDecodingConfig, ContinuousBatchEngine, LlmInferenceEngine,
};
use ferrum_scheduler::implementations::ContinuousBatchScheduler;
use ferrum_testkit::{
ConfigurableModelExecutor, MockKvCacheManager, MockSampler, MockTensorFactory, MockTokenizer,
};
use ferrum_types::{InferenceRequest, SchedulerConfig};
const VOCAB: usize = 1000;
fn make_engine_speculative(
num_spec_tokens: usize,
draft_seq: Vec<u32>,
target_seq: Vec<u32>,
) -> ContinuousBatchEngine {
let config = ferrum_types::EngineConfig::default();
let scheduler = Arc::new(ContinuousBatchScheduler::new(SchedulerConfig::default()));
let tokenizer = Arc::new(MockTokenizer::new(VOCAB));
let sampler = Arc::new(MockSampler);
let kv_cache = Arc::new(MockKvCacheManager::new(1024));
let tensor_factory = Arc::new(MockTensorFactory);
let target = Arc::new(ConfigurableModelExecutor::with_token_sequence(
VOCAB, target_seq,
));
let draft = Arc::new(ConfigurableModelExecutor::with_token_sequence(
VOCAB, draft_seq,
));
let cfg = SpeculativeDecodingConfig {
num_speculative_tokens: num_spec_tokens,
temperature: 1.0,
};
ContinuousBatchEngine::new_with_speculation(
config,
scheduler,
tokenizer,
sampler,
kv_cache,
target,
tensor_factory,
Some(draft),
Some(cfg),
)
.expect("legacy engine composition must match executor authority")
}
fn make_request(prompt: &str, max_tokens: usize) -> InferenceRequest {
let mut req = InferenceRequest::new(prompt, "mock-model");
req.sampling_params.max_tokens = max_tokens;
req.sampling_params.temperature = 0.0;
req
}
#[tokio::test]
async fn speculative_agreement_generates_expected_tokens() {
let engine = make_engine_speculative(3, vec![42, 42, 42, 42, 42], vec![42, 42, 42, 42, 42]);
let req = make_request("hello", 4);
let resp = engine.infer(req).await.expect("infer");
assert_eq!(
resp.tokens.len(),
4,
"should generate exactly max_tokens={} tokens, got {}",
4,
resp.tokens.len()
);
for tok in &resp.tokens {
assert_eq!(
tok.get(),
42,
"agreeing draft+target should emit token 42 each time"
);
}
}
#[tokio::test]
async fn speculative_disagreement_still_produces_output() {
let engine = make_engine_speculative(3, vec![7, 7, 7, 7, 7], vec![21, 21, 21, 21, 21]);
let req = make_request("hello", 3);
let resp = engine.infer(req).await.expect("infer");
assert!(!resp.tokens.is_empty(), "engine should produce tokens");
for tok in &resp.tokens {
assert_eq!(
tok.get(),
21,
"disagreement case should resolve to target-residual pick"
);
}
}
#[tokio::test]
async fn speculative_output_matches_non_speculative_for_identical_models() {
let baseline_engine = {
let config = ferrum_types::EngineConfig::default();
let scheduler = Arc::new(ContinuousBatchScheduler::new(SchedulerConfig::default()));
let tokenizer = Arc::new(MockTokenizer::new(VOCAB));
let sampler = Arc::new(MockSampler);
let kv_cache = Arc::new(MockKvCacheManager::new(1024));
let tensor_factory = Arc::new(MockTensorFactory);
let executor = Arc::new(ConfigurableModelExecutor::with_token_sequence(
VOCAB,
vec![9, 9, 9, 9, 9],
));
ContinuousBatchEngine::new(
config,
scheduler,
tokenizer,
sampler,
kv_cache,
executor,
tensor_factory,
)
.expect("legacy engine composition must match executor authority")
};
let baseline_resp = baseline_engine
.infer(make_request("hi", 4))
.await
.expect("baseline infer");
let spec_engine = make_engine_speculative(2, vec![9, 9, 9, 9, 9], vec![9, 9, 9, 9, 9]);
let spec_resp = spec_engine
.infer(make_request("hi", 4))
.await
.expect("spec infer");
assert_eq!(
baseline_resp.tokens.len(),
spec_resp.tokens.len(),
"identical draft/target spec should match baseline count"
);
for (b, s) in baseline_resp.tokens.iter().zip(spec_resp.tokens.iter()) {
assert_eq!(
b.get(),
s.get(),
"per-token match between baseline and speculative"
);
}
}