//! Serving Engine for Continuous Batching //! //! This module provides the main serving engine that coordinates //! request submission, scheduling, and model execution with streaming output. use super::kv_cache_manager::KvCachePoolConfig; use super::request::{ CompletedRequest, FinishReason, InferenceRequest, Priority, RequestId, RequestState, RunningRequest, TokenOutput, }; use super::scheduler::{ContinuousBatchScheduler, RequestQueue, SchedulerConfig}; use crate::backends::{GenerateParams, GeneratedToken, LlmBackend}; use crate::error::{Result, RuvLLMError}; use crate::optimization::realtime::RealtimeOptimizer; use crate::speculative::{SpeculativeConfig, SpeculativeDecoder}; use parking_lot::{Mutex, RwLock}; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; #[cfg(feature = "async-runtime")] use tokio::sync::mpsc; /// Configuration for the serving engine #[derive(Debug, Clone)] pub struct ServingEngineConfig { /// Scheduler configuration pub scheduler: SchedulerConfig, /// KV cache pool configuration pub kv_cache: KvCachePoolConfig, /// Maximum concurrent requests pub max_concurrent_requests: usize, /// Enable request coalescing pub coalesce_requests: bool, /// Coalescing window in milliseconds pub coalesce_window_ms: u64, /// Enable streaming output pub streaming_enabled: bool, /// Request timeout in milliseconds pub request_timeout_ms: u64, /// Enable speculative decoding (default: true for 2-3x speedup) pub enable_speculative: bool, /// Speculative decoding configuration pub speculative_config: SpeculativeConfig, /// Draft model path for speculative decoding (auto-detected if None) /// - For 7B+ models: use 1B draft (e.g., "TinyLlama/TinyLlama-1.1B-Chat-v1.0") /// - For 3B models: use 0.5B draft (e.g., "Qwen/Qwen2.5-0.5B") pub draft_model_path: Option, } impl Default for ServingEngineConfig { fn default() -> Self { Self { scheduler: SchedulerConfig::default(), kv_cache: KvCachePoolConfig::default(), max_concurrent_requests: 256, coalesce_requests: false, coalesce_window_ms: 10, streaming_enabled: true, request_timeout_ms: 60000, enable_speculative: true, // Enabled by default for 2-3x decode speedup speculative_config: SpeculativeConfig::default(), draft_model_path: None, // Auto-detected based on main model size } } } /// Result of processing a request #[derive(Debug, Clone)] pub struct GenerationResult { /// Request ID pub request_id: RequestId, /// Generated token IDs pub generated_tokens: Vec, /// Generated text (if decoded) pub generated_text: Option, /// Finish reason pub finish_reason: FinishReason, /// Processing time in milliseconds pub processing_time_ms: u64, /// Tokens per second pub tokens_per_second: f64, /// Number of prompt tokens pub prompt_tokens: usize, /// Number of generated tokens pub completion_tokens: usize, } impl From for GenerationResult { fn from(completed: CompletedRequest) -> Self { Self { request_id: completed.id, generated_tokens: completed.generated_tokens.clone(), generated_text: None, finish_reason: completed.finish_reason, processing_time_ms: completed.processing_time_ms, tokens_per_second: completed.tokens_per_second, prompt_tokens: completed.prompt_tokens.len(), completion_tokens: completed.generated_tokens.len(), } } } /// Streaming token callback pub type TokenCallback = Box; /// Internal request state for the engine struct EngineRequest { /// Request data request: InferenceRequest, /// Token callback for streaming callback: Option, /// Completion notifier #[cfg(feature = "async-runtime")] completion_tx: Option>, /// Created time created_at: Instant, } /// The serving engine for continuous batching pub struct ServingEngine { /// Configuration config: ServingEngineConfig, /// The LLM backend model: Arc, /// Draft model for speculative decoding (loaded lazily) draft_model: RwLock>>, /// Request scheduler scheduler: Mutex, /// Request queue queue: Mutex, /// Pending request data pending_requests: RwLock>, /// Completed results completed_results: RwLock>, /// Running state is_running: AtomicBool, /// Total requests processed total_requests: AtomicU64, /// Total tokens generated total_tokens: AtomicU64, /// Start time for metrics start_time: Instant, /// Realtime optimizer for speculative decoding decisions optimizer: RealtimeOptimizer, } impl ServingEngine { /// Create a new serving engine pub fn new(model: Arc, config: ServingEngineConfig) -> Self { use crate::optimization::realtime::RealtimeConfig; let scheduler = ContinuousBatchScheduler::new(config.scheduler.clone(), config.kv_cache.clone()); // Create realtime optimizer with speculative decoding enabled by default let realtime_config = RealtimeConfig { enable_speculative: config.enable_speculative, speculative: crate::optimization::realtime::SpeculativeConfig { draft_model: config.draft_model_path.clone(), num_speculative_tokens: config.speculative_config.lookahead, acceptance_threshold: config.speculative_config.acceptance_threshold, tree_speculation: config.speculative_config.tree_speculation, max_tree_depth: config.speculative_config.max_tree_depth, }, ..Default::default() }; let optimizer = RealtimeOptimizer::new(realtime_config); Self { config, model, draft_model: RwLock::new(None), scheduler: Mutex::new(scheduler), queue: Mutex::new(RequestQueue::new()), pending_requests: RwLock::new(HashMap::new()), completed_results: RwLock::new(HashMap::new()), is_running: AtomicBool::new(false), total_requests: AtomicU64::new(0), total_tokens: AtomicU64::new(0), start_time: Instant::now(), optimizer, } } /// Create with default configuration pub fn with_default_config(model: Arc) -> Self { Self::new(model, ServingEngineConfig::default()) } /// Submit a request for processing pub fn submit(&self, request: InferenceRequest) -> Result { let request_id = request.id; // Check capacity { let queue = self.queue.lock(); if queue.pending_count() + queue.running_count() >= self.config.max_concurrent_requests { return Err(RuvLLMError::OutOfMemory( "Maximum concurrent requests reached".to_string(), )); } } // Store request data { let engine_request = EngineRequest { request: request.clone(), callback: None, #[cfg(feature = "async-runtime")] completion_tx: None, created_at: Instant::now(), }; self.pending_requests .write() .insert(request_id, engine_request); } // Add to queue self.queue.lock().add(request); self.total_requests.fetch_add(1, Ordering::Relaxed); Ok(request_id) } /// Submit a request with a streaming callback pub fn submit_with_callback( &self, request: InferenceRequest, callback: TokenCallback, ) -> Result { let request_id = request.id; // Check capacity { let queue = self.queue.lock(); if queue.pending_count() + queue.running_count() >= self.config.max_concurrent_requests { return Err(RuvLLMError::OutOfMemory( "Maximum concurrent requests reached".to_string(), )); } } // Store request data with callback { let engine_request = EngineRequest { request: request.clone(), callback: Some(callback), #[cfg(feature = "async-runtime")] completion_tx: None, created_at: Instant::now(), }; self.pending_requests .write() .insert(request_id, engine_request); } // Add to queue self.queue.lock().add(request); self.total_requests.fetch_add(1, Ordering::Relaxed); Ok(request_id) } /// Get the result of a completed request pub fn get_result(&self, id: RequestId) -> Option { self.completed_results.write().remove(&id) } /// Check if a request is complete pub fn is_complete(&self, id: RequestId) -> bool { self.completed_results.read().contains_key(&id) } /// Cancel a request pub fn cancel(&self, id: RequestId) -> bool { // Try to remove from pending if self.pending_requests.write().remove(&id).is_some() { // Remove from queue if still pending let mut queue = self.queue.lock(); queue.pending.retain(|r| r.id != id); return true; } // Try to remove from running let mut queue = self.queue.lock(); if let Some(running) = queue.remove_running(id) { // Free KV cache self.scheduler.lock().kv_cache_manager_mut().free(id); // Create cancelled result - extract values before moving generated_tokens let completion_tokens = running.generated_tokens.len(); let processing_time_ms = running.processing_time().as_millis() as u64; let tokens_per_second = running.tokens_per_second(); let prompt_tokens = running.request.prompt_len(); let result = GenerationResult { request_id: id, generated_tokens: running.generated_tokens, generated_text: None, finish_reason: FinishReason::Cancelled, processing_time_ms, tokens_per_second, prompt_tokens, completion_tokens, }; self.completed_results.write().insert(id, result); return true; } false } /// Run a single iteration of the serving loop /// /// Returns the generated tokens for this iteration pub fn run_iteration(&self) -> Result> { let mut outputs = Vec::new(); // Schedule next batch let batch = { let mut queue = self.queue.lock(); let mut scheduler = self.scheduler.lock(); scheduler.schedule(&mut queue) }; if batch.is_empty() { return Ok(outputs); } // Process the batch (this is where the actual model inference would happen) // For now, we simulate token generation // Process each request in the batch for batched_req in &batch.requests { let request_id = batched_req.request_id; if batched_req.is_prefill { // Prefill complete - update state let mut queue = self.queue.lock(); if let Some(running) = queue.get_running_mut(request_id) { if !running.prefill_complete { running.advance_prefill(batched_req.token_ids.len()); } } } else { // Decode - generate a token using the real model let generated_token = { let queue = self.queue.lock(); if let Some(running) = queue.running.get(&request_id) { self.generate_next_token(request_id, running)? } else { // Request not found, skip continue; } }; let mut queue = self.queue.lock(); if let Some(running) = queue.get_running_mut(request_id) { running.add_token(generated_token); // Create output let output = TokenOutput { request_id, token_id: generated_token, token_text: None, // Would decode with tokenizer logprob: None, is_final: running.is_complete(), finish_reason: if running.is_complete() { Some(FinishReason::Length) } else { None }, seq_len: running.current_seq_len, }; // Send to callback if registered if let Some(engine_req) = self.pending_requests.read().get(&request_id) { if let Some(callback) = &engine_req.callback { callback(output.clone()); } } outputs.push(output); // Update KV cache length let _ = self .scheduler .lock() .kv_cache_manager_mut() .set_length(request_id, running.current_seq_len); self.total_tokens.fetch_add(1, Ordering::Relaxed); // Check if complete if running.is_complete() { // Will handle completion below } } } } // Handle completions self.handle_completions()?; Ok(outputs) } /// Handle completed requests fn handle_completions(&self) -> Result<()> { let mut completed_ids = Vec::new(); // Find completed requests { let queue = self.queue.lock(); for (id, running) in &queue.running { if running.is_complete() { completed_ids.push(*id); } } } // Process completions for id in completed_ids { let running = { let mut queue = self.queue.lock(); queue.remove_running(id) }; if let Some(running) = running { // Free KV cache self.scheduler.lock().kv_cache_manager_mut().free(id); // Create result let result = GenerationResult { request_id: id, generated_tokens: running.generated_tokens.clone(), generated_text: None, finish_reason: FinishReason::Length, processing_time_ms: running.processing_time().as_millis() as u64, tokens_per_second: running.tokens_per_second(), prompt_tokens: running.request.prompt_len(), completion_tokens: running.generated_tokens.len(), }; // Store result self.completed_results.write().insert(id, result.clone()); // Send final callback if let Some(engine_req) = self.pending_requests.write().remove(&id) { if let Some(callback) = &engine_req.callback { callback(TokenOutput { request_id: id, token_id: running.generated_tokens.last().copied().unwrap_or(0), token_text: None, logprob: None, is_final: true, finish_reason: Some(FinishReason::Length), seq_len: running.current_seq_len, }); } #[cfg(feature = "async-runtime")] if let Some(tx) = engine_req.completion_tx { let _ = tx.send(result); } } } } Ok(()) } /// Generate next token using the model backend /// /// This method implements real autoregressive token generation: /// 1. Gets the current context (prompt + generated tokens) /// 2. Runs a forward pass through the model /// 3. Applies sampling (temperature, top-p, top-k) /// 4. Uses speculative decoding when available for 2-3x speedup /// /// # Arguments /// * `request_id` - The request ID to generate for /// * `running` - The running request state /// /// # Returns /// The generated token ID fn generate_next_token(&self, request_id: RequestId, running: &RunningRequest) -> Result { // Build the context: prompt tokens + already generated tokens let mut context = running.request.prompt_tokens.clone(); context.extend(&running.generated_tokens); // Get generation parameters from the request let params = &running.request.params; // Check if we should use speculative decoding if self.should_use_speculative(params) { if let Some(draft_model) = self.draft_model.read().as_ref() { // Speculative decoding available - use it for faster generation return self.generate_with_speculation(request_id, &context, params, draft_model); } } // Standard single-token generation via model backend self.generate_single_token(&context, params) } /// Generate a single token using standard autoregressive decoding fn generate_single_token( &self, context: &[u32], params: &crate::backends::GenerateParams, ) -> Result { // Check if model is loaded - if not, fall back to simulation for testing if !self.model.is_model_loaded() { // No model loaded - simulate token generation for testing // In production this should be an error, but for tests without // a real model we return a pseudo-random token based on context let hash = context .iter() .fold(0u32, |acc, &t| acc.wrapping_add(t).wrapping_mul(31)); return Ok(hash % 32000); } // Decode context to text for the backend let context_text = if let Some(tokenizer) = self.model.tokenizer() { tokenizer.decode(context)? } else { // No tokenizer but model is loaded - try direct generation // and extract token from the generated text return Err(RuvLLMError::InvalidOperation( "No tokenizer available for text decoding".to_string(), )); }; // Generate one token using the backend let gen_params = crate::backends::GenerateParams { max_tokens: 1, temperature: params.temperature, top_p: params.top_p, top_k: params.top_k, repetition_penalty: params.repetition_penalty, frequency_penalty: params.frequency_penalty, presence_penalty: params.presence_penalty, stop_sequences: vec![], // Don't stop on sequences for single token seed: params.seed, }; // Generate text (single token) let generated_text = self.model.generate(&context_text, gen_params)?; // Tokenize the result to get the new token if let Some(tokenizer) = self.model.tokenizer() { let full_text = format!("{}{}", context_text, generated_text); let full_tokens = tokenizer.encode(&full_text)?; // The new token is at position context.len() if full_tokens.len() > context.len() { return Ok(full_tokens[context.len()]); } // If no new token generated, return EOS if let Some(eos) = tokenizer.special_tokens().eos_token_id { return Ok(eos); } } Err(RuvLLMError::Generation( "Failed to generate token".to_string(), )) } /// Generate tokens using speculative decoding for 2-3x speedup /// /// Speculative decoding works by: /// 1. Using a small draft model to predict K tokens ahead /// 2. Verifying all K tokens with the main model in a single forward pass /// 3. Accepting matching tokens and correcting where they diverge fn generate_with_speculation( &self, _request_id: RequestId, context: &[u32], params: &crate::backends::GenerateParams, draft_model: &Arc, ) -> Result { let spec_config = &self.config.speculative_config; let lookahead = spec_config.lookahead; // Get tokenizer for encoding/decoding let tokenizer = self .model .tokenizer() .ok_or_else(|| RuvLLMError::InvalidOperation("No tokenizer available".to_string()))?; // Decode context to text let context_text = tokenizer.decode(context)?; // Draft phase: generate K tokens with the small model let draft_params = crate::backends::GenerateParams { max_tokens: lookahead, temperature: spec_config.draft_temperature, top_p: spec_config.draft_top_p, top_k: if spec_config.draft_temperature == 0.0 { 1 } else { 40 }, ..Default::default() }; let draft_text = draft_model.generate(&context_text, draft_params)?; let draft_full = format!("{}{}", context_text, draft_text); let draft_tokens = tokenizer.encode(&draft_full)?; // Extract draft tokens (beyond original context) let draft_new: Vec = draft_tokens .iter() .skip(context.len()) .take(lookahead) .copied() .collect(); if draft_new.is_empty() { // Draft model couldn't generate, fall back to single token return self.generate_single_token(context, params); } // Verify phase: check draft tokens with main model // Build context with draft tokens for verification let mut verify_context = context.to_vec(); for (i, &draft_token) in draft_new.iter().enumerate() { let verify_text = tokenizer.decode(&verify_context)?; let verify_params = crate::backends::GenerateParams { max_tokens: 1, temperature: params.temperature, top_p: params.top_p, top_k: params.top_k, ..params.clone() }; let main_text = self.model.generate(&verify_text, verify_params)?; let main_full = format!("{}{}", verify_text, main_text); let main_tokens = tokenizer.encode(&main_full)?; if main_tokens.len() <= verify_context.len() { // Main model produced nothing, return EOS or use draft if let Some(eos) = tokenizer.special_tokens().eos_token_id { return Ok(eos); } return Ok(draft_token); } let main_token = main_tokens[verify_context.len()]; if main_token == draft_token { // Accept draft token verify_context.push(draft_token); } else { // Reject - return main model's correction // Record stats through optimizer self.optimizer.update_speculation_stats(i, draft_new.len()); return Ok(main_token); } } // All drafts accepted - get one more token from main model let final_text = tokenizer.decode(&verify_context)?; let final_params = crate::backends::GenerateParams { max_tokens: 1, temperature: params.temperature, top_p: params.top_p, top_k: params.top_k, ..params.clone() }; let continuation = self.model.generate(&final_text, final_params)?; let continuation_full = format!("{}{}", final_text, continuation); let continuation_tokens = tokenizer.encode(&continuation_full)?; // Record successful speculation self.optimizer .update_speculation_stats(draft_new.len(), draft_new.len()); if continuation_tokens.len() > verify_context.len() { Ok(continuation_tokens[verify_context.len()]) } else if let Some(eos) = tokenizer.special_tokens().eos_token_id { Ok(eos) } else { Err(RuvLLMError::Generation( "Failed to generate continuation token".to_string(), )) } } /// Generate tokens with streaming callback support /// /// This method generates tokens one at a time, calling the provided /// callback for each token. Useful for real-time output display. pub fn generate_with_callback( &self, request: &InferenceRequest, mut callback: F, ) -> Result> where F: FnMut(TokenOutput) -> bool, // Returns false to stop generation { let mut context = request.prompt_tokens.clone(); let mut generated = Vec::new(); let params = &request.params; let eos_token = self .model .tokenizer() .and_then(|t| t.special_tokens().eos_token_id); while generated.len() < params.max_tokens { // Generate next token let token = self.generate_single_token(&context, params)?; // Check for EOS if Some(token) == eos_token { let output = TokenOutput { request_id: request.id, token_id: token, token_text: self.decode_token(token), logprob: None, is_final: true, finish_reason: Some(FinishReason::EndOfSequence), seq_len: context.len() + 1, }; callback(output); break; } // Update context context.push(token); generated.push(token); // Create output and call callback let is_final = generated.len() >= params.max_tokens; let output = TokenOutput { request_id: request.id, token_id: token, token_text: self.decode_token(token), logprob: None, is_final, finish_reason: if is_final { Some(FinishReason::Length) } else { None }, seq_len: context.len(), }; // Check if callback wants to stop if !callback(output) { break; } // Check stop sequences if !params.stop_sequences.is_empty() { if let Some(tokenizer) = self.model.tokenizer() { if let Ok(generated_text) = tokenizer.decode(&generated) { for stop_seq in ¶ms.stop_sequences { if generated_text.contains(stop_seq) { return Ok(generated); } } } } } } Ok(generated) } /// Decode a single token to text (helper method) fn decode_token(&self, token: u32) -> Option { self.model.tokenizer().and_then(|t| t.decode(&[token]).ok()) } /// Run the serving loop until stopped pub fn run(&self) -> Result<()> { self.is_running.store(true, Ordering::SeqCst); while self.is_running.load(Ordering::SeqCst) { // Check if there's work to do let has_work = { let queue = self.queue.lock(); !queue.is_empty() }; if has_work { self.run_iteration()?; } else { // No work, yield std::thread::sleep(Duration::from_micros(100)); } // Check for timeout requests self.check_timeouts(); } Ok(()) } /// Stop the serving loop pub fn stop(&self) { self.is_running.store(false, Ordering::SeqCst); } /// Check for and handle timed out requests fn check_timeouts(&self) { let timeout = Duration::from_millis(self.config.request_timeout_ms); let mut timed_out = Vec::new(); // Find timed out pending requests { let pending = self.pending_requests.read(); for (id, req) in pending.iter() { if req.created_at.elapsed() > timeout { timed_out.push(*id); } } } // Cancel timed out requests for id in timed_out { self.cancel(id); } } /// Get serving metrics pub fn metrics(&self) -> ServingMetrics { let queue = self.queue.lock(); let scheduler = self.scheduler.lock(); let elapsed = self.start_time.elapsed().as_secs_f64(); let total_requests = self.total_requests.load(Ordering::Relaxed); let total_tokens = self.total_tokens.load(Ordering::Relaxed); let completed_count = self.completed_results.read().len(); ServingMetrics { requests_per_second: if elapsed > 0.0 { total_requests as f64 / elapsed } else { 0.0 }, tokens_per_second: if elapsed > 0.0 { total_tokens as f64 / elapsed } else { 0.0 }, average_latency_ms: 0.0, // Would need to track per-request latencies p99_latency_ms: 0.0, // Would need latency histogram batch_utilization: 0.0, // Would need to track batch sizes kv_cache_utilization: scheduler.kv_cache_manager().stats().slot_utilization(), pending_requests: queue.pending_count(), running_requests: queue.running_count(), completed_requests: completed_count, total_requests_processed: total_requests, total_tokens_generated: total_tokens, uptime_seconds: elapsed, } } /// Get serving statistics (alias for metrics) pub fn stats(&self) -> ServingMetrics { self.metrics() } /// Get configuration pub fn config(&self) -> &ServingEngineConfig { &self.config } /// Check if speculative decoding should be used for the given generation params /// /// Returns true when: /// - Speculative decoding is enabled in config /// - Temperature is low (< 0.5) for deterministic generation /// - Greedy decoding (top_k = 1) /// - A draft model is available or can be loaded pub fn should_use_speculative(&self, params: &GenerateParams) -> bool { if !self.config.enable_speculative { return false; } // Use the optimizer's recommendation self.optimizer.should_use_speculative(params) } /// Get recommended draft model path based on main model size /// /// Auto-detection rules: /// - For 7B+ models: use 1B draft (e.g., TinyLlama-1.1B) /// - For 3B models: use 0.5B draft (e.g., Qwen2.5-0.5B) /// - Returns configured path if explicitly set pub fn get_draft_model_path(&self) -> Option { // Return configured path if explicitly set if let Some(ref path) = self.config.draft_model_path { return Some(path.clone()); } // Auto-detect based on main model info if let Some(info) = self.model.model_info() { let params_billions = info.num_parameters as f64 / 1_000_000_000.0; if params_billions >= 7.0 { // 7B+ models: use 1B draft model Some("TinyLlama/TinyLlama-1.1B-Chat-v1.0".to_string()) } else if params_billions >= 3.0 { // 3B models: use 0.5B draft model Some("Qwen/Qwen2.5-0.5B".to_string()) } else { // For smaller models, speculative decoding overhead may not be worth it None } } else { // No model info available, use sensible default Some("TinyLlama/TinyLlama-1.1B-Chat-v1.0".to_string()) } } /// Set the draft model for speculative decoding pub fn set_draft_model(&self, draft_model: Arc) { *self.draft_model.write() = Some(draft_model); // Enable speculative decoding in the optimizer if let Some(path) = self.get_draft_model_path() { self.optimizer.enable_speculative_decoding(&path); } } /// Get the realtime optimizer for advanced optimization decisions pub fn optimizer(&self) -> &RealtimeOptimizer { &self.optimizer } /// Get speculative decoding statistics pub fn speculative_stats(&self) -> Option { // TODO: Return actual stats when speculative decoder is integrated // For now, return placeholder stats if self.optimizer.is_speculative_active() { Some(crate::speculative::SpeculativeStats { draft_tokens: 0, accepted_tokens: 0, acceptance_rate: 0.0, speedup: 1.0, main_forward_passes: 0, draft_forward_passes: 0, avg_tokens_per_main_pass: 1.0, total_speculation_time_ms: 0.0, total_tokens_generated: 0, }) } else { None } } } /// Serving metrics #[derive(Debug, Clone, Default)] pub struct ServingMetrics { /// Requests processed per second pub requests_per_second: f64, /// Tokens generated per second pub tokens_per_second: f64, /// Average request latency in milliseconds pub average_latency_ms: f64, /// 99th percentile latency in milliseconds pub p99_latency_ms: f64, /// Batch utilization (0.0 - 1.0) pub batch_utilization: f64, /// KV cache utilization (0.0 - 1.0) pub kv_cache_utilization: f64, /// Number of pending requests pub pending_requests: usize, /// Number of running requests pub running_requests: usize, /// Number of completed requests pub completed_requests: usize, /// Total requests processed pub total_requests_processed: u64, /// Total tokens generated pub total_tokens_generated: u64, /// Uptime in seconds pub uptime_seconds: f64, } // ============================================================================ // Async support // ============================================================================ #[cfg(feature = "async-runtime")] impl ServingEngine { /// Submit a request and await completion pub async fn submit_async(&self, request: InferenceRequest) -> Result { let request_id = request.id; let (tx, rx) = tokio::sync::oneshot::channel(); // Store request with completion channel { let engine_request = EngineRequest { request: request.clone(), callback: None, completion_tx: Some(tx), created_at: Instant::now(), }; self.pending_requests .write() .insert(request_id, engine_request); } // Add to queue self.queue.lock().add(request); self.total_requests.fetch_add(1, Ordering::Relaxed); // Wait for completion rx.await .map_err(|_| RuvLLMError::Generation("Request cancelled".to_string())) } /// Stream tokens for a request pub fn stream( &self, request: InferenceRequest, ) -> Result> { let (tx, rx) = mpsc::unbounded_channel(); let request_id = request.id; // Create callback that sends to channel let callback: TokenCallback = Box::new(move |output| { let _ = tx.send(output); }); // Submit with callback self.submit_with_callback(request, callback)?; Ok(tokio_stream::wrappers::UnboundedReceiverStream::new(rx)) } /// Run the serving loop asynchronously pub async fn run_async(&self) -> Result<()> { self.is_running.store(true, Ordering::SeqCst); while self.is_running.load(Ordering::SeqCst) { let has_work = { let queue = self.queue.lock(); !queue.is_empty() }; if has_work { self.run_iteration()?; } else { tokio::time::sleep(Duration::from_micros(100)).await; } self.check_timeouts(); } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::backends::NoopBackend; fn create_test_engine() -> ServingEngine { let model = Arc::new(NoopBackend); let config = ServingEngineConfig { kv_cache: KvCachePoolConfig { num_slots: 4, max_seq_len: 256, block_size: 16, total_blocks: 64, num_kv_heads: 2, head_dim: 64, num_layers: 4, }, ..Default::default() }; ServingEngine::new(model, config) } fn create_test_request() -> InferenceRequest { let params = GenerateParams::default().with_max_tokens(10); InferenceRequest::new(vec![1, 2, 3, 4, 5], params) } #[test] fn test_submit_request() { let engine = create_test_engine(); let request = create_test_request(); let id = request.id; let result = engine.submit(request); assert!(result.is_ok()); assert_eq!(result.unwrap(), id); } #[test] fn test_cancel_request() { let engine = create_test_engine(); let request = create_test_request(); let id = engine.submit(request).unwrap(); let cancelled = engine.cancel(id); assert!(cancelled); } #[test] fn test_run_iteration() { let engine = create_test_engine(); let request = create_test_request(); engine.submit(request).unwrap(); // First iteration should do prefill let outputs = engine.run_iteration().unwrap(); // May or may not have outputs depending on scheduler behavior } #[test] fn test_metrics() { let engine = create_test_engine(); let metrics = engine.metrics(); assert_eq!(metrics.pending_requests, 0); assert_eq!(metrics.running_requests, 0); } #[test] fn test_with_callback() { use std::sync::atomic::AtomicUsize; let engine = create_test_engine(); let request = create_test_request(); let callback_count = Arc::new(AtomicUsize::new(0)); let count_clone = callback_count.clone(); let callback: TokenCallback = Box::new(move |_| { count_clone.fetch_add(1, Ordering::Relaxed); }); let id = engine.submit_with_callback(request, callback).unwrap(); // Run a few iterations for _ in 0..15 { let _ = engine.run_iteration(); } // Callback should have been called at least once // (actual count depends on scheduling and token generation) } #[test] fn test_token_generation_with_noop_backend() { // Test that token generation works (via simulation) even with NoopBackend let engine = create_test_engine(); let request = create_test_request(); engine.submit(request).unwrap(); // Run multiple iterations to process prefill and generate tokens for _ in 0..20 { let result = engine.run_iteration(); // Should not error even without a real model assert!(result.is_ok()); } let stats = engine.stats(); // Should have processed at least one request assert!( stats.running_requests > 0 || stats.completed_requests > 0 || stats.pending_requests > 0 ); } #[test] fn test_generation_produces_different_tokens() { // Test that different contexts produce different tokens let engine = create_test_engine(); // Submit requests with different prompt tokens let params1 = GenerateParams::default().with_max_tokens(5); let request1 = InferenceRequest::new(vec![1, 2, 3], params1); let params2 = GenerateParams::default().with_max_tokens(5); let request2 = InferenceRequest::new(vec![100, 200, 300], params2); let id1 = engine.submit(request1).unwrap(); let id2 = engine.submit(request2).unwrap(); // Run iterations for _ in 0..30 { let _ = engine.run_iteration(); } // Both requests should have been processed let stats = engine.stats(); // At minimum we should have started processing } #[test] fn test_speculative_config_defaults() { // Test that speculative decoding config has sensible defaults let config = ServingEngineConfig::default(); // Speculative decoding should be enabled by default assert!(config.enable_speculative); // Default lookahead should be reasonable (4-8 tokens) assert!(config.speculative_config.lookahead >= 2); assert!(config.speculative_config.lookahead <= 16); // Draft temperature should be low for deterministic drafting assert!(config.speculative_config.draft_temperature <= 0.5); } #[test] fn test_streaming_generation() { // Test streaming generation with callbacks use std::sync::atomic::AtomicUsize; let engine = create_test_engine(); let params = GenerateParams::default() .with_max_tokens(5) .with_temperature(0.8); let request = InferenceRequest::new(vec![1, 2, 3, 4, 5], params); let tokens_received = Arc::new(AtomicUsize::new(0)); let tokens_clone = tokens_received.clone(); let callback: TokenCallback = Box::new(move |output| { tokens_clone.fetch_add(1, Ordering::Relaxed); // Verify token output has valid fields assert!(output.seq_len > 0); }); engine.submit_with_callback(request, callback).unwrap(); // Run iterations for _ in 0..30 { let _ = engine.run_iteration(); } // Should have received at least some tokens // (exact count depends on prefill/decode scheduling) } #[test] fn test_generation_respects_max_tokens() { let engine = create_test_engine(); // Request with small max_tokens let params = GenerateParams::default().with_max_tokens(3); let request = InferenceRequest::new(vec![1, 2, 3], params); engine.submit(request).unwrap(); // Run many iterations for _ in 0..50 { let _ = engine.run_iteration(); } // Check metrics - request should complete let stats = engine.stats(); // Either completed or still processing, but should not hang } #[test] fn test_deterministic_generation_with_seed() { // Test that the same context produces consistent results let engine = create_test_engine(); // Two identical requests let params = GenerateParams::default().with_max_tokens(5).with_seed(42); let request1 = InferenceRequest::new(vec![10, 20, 30], params.clone()); let request2 = InferenceRequest::new(vec![10, 20, 30], params); engine.submit(request1).unwrap(); engine.submit(request2).unwrap(); // Process both for _ in 0..30 { let _ = engine.run_iteration(); } // Both should complete successfully } }