//! Real-time Optimization for LLM Inference //! //! Features: //! - Dynamic batch sizing based on latency targets //! - KV cache pressure management //! - Token budget allocation //! - Speculative decoding integration use crate::error::{Result, RuvLLMError}; use crate::optimization::metrics::{InferenceMetrics, MetricsSnapshot}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::time::{Duration, Instant}; /// Configuration for the realtime optimizer #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RealtimeConfig { /// Target latency for TTFT (milliseconds) pub latency_target_ms: f32, /// Target throughput (tokens per second) pub throughput_target_tps: f32, /// Minimum batch size pub min_batch_size: usize, /// Maximum batch size pub max_batch_size: usize, /// KV cache pressure threshold (0.0 - 1.0) pub kv_cache_pressure_threshold: f32, /// Enable speculative decoding pub enable_speculative: bool, /// Speculative decoding configuration pub speculative: SpeculativeConfig, /// Batch sizing strategy pub batch_strategy: BatchSizeStrategy, /// KV cache pressure policy pub kv_policy: KvCachePressurePolicy, /// Maximum memory budget (bytes) pub max_memory_bytes: usize, /// Optimization interval (how often to recompute decisions) pub optimization_interval_ms: u64, } impl Default for RealtimeConfig { fn default() -> Self { Self { latency_target_ms: 100.0, throughput_target_tps: 50.0, min_batch_size: 1, max_batch_size: 64, kv_cache_pressure_threshold: 0.8, enable_speculative: true, // Enabled by default for 2-3x decode speedup speculative: SpeculativeConfig::default(), batch_strategy: BatchSizeStrategy::Adaptive, kv_policy: KvCachePressurePolicy::Evict, max_memory_bytes: 8 * 1024 * 1024 * 1024, // 8GB default optimization_interval_ms: 100, } } } /// Batch size selection strategy #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum BatchSizeStrategy { /// Fixed batch size Fixed, /// Adaptive based on latency Adaptive, /// Aggressive (maximize throughput) Aggressive, /// Conservative (minimize latency) Conservative, /// Hybrid (balance throughput and latency) Hybrid, } impl Default for BatchSizeStrategy { fn default() -> Self { Self::Adaptive } } /// Policy for handling KV cache pressure #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum KvCachePressurePolicy { /// Evict oldest entries Evict, /// Quantize more aggressively Quantize, /// Reject new requests Reject, /// Spill to disk Spill, /// Hybrid approach Hybrid, } impl Default for KvCachePressurePolicy { fn default() -> Self { Self::Evict } } /// Configuration for speculative decoding #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SpeculativeConfig { /// Draft model identifier pub draft_model: Option, /// Number of speculative tokens pub num_speculative_tokens: usize, /// Acceptance threshold pub acceptance_threshold: f32, /// Enable tree-based speculation pub tree_speculation: bool, /// Maximum tree depth pub max_tree_depth: usize, } impl Default for SpeculativeConfig { fn default() -> Self { Self { draft_model: None, num_speculative_tokens: 4, acceptance_threshold: 0.8, tree_speculation: false, max_tree_depth: 3, } } } /// Token budget allocation for a request #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenBudgetAllocation { /// Request identifier pub request_id: String, /// Maximum tokens to generate pub max_tokens: usize, /// Priority level (higher = more resources) pub priority: f32, /// Deadline (optional) pub deadline: Option, /// Allocated batch slot pub batch_slot: Option, /// Estimated completion time pub estimated_completion_ms: f32, } /// Request representation for optimization #[derive(Debug, Clone)] pub struct Request { /// Request identifier pub id: String, /// Input token count pub input_tokens: usize, /// Maximum output tokens pub max_output_tokens: usize, /// Priority (0.0 - 1.0) pub priority: f32, /// Arrival time pub arrival_time: Instant, /// Deadline (optional) pub deadline: Option, } /// Optimization decision output #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OptimizationDecision { /// Recommended batch size pub batch_size: usize, /// Whether to evict KV cache pub should_evict_kv_cache: bool, /// Number of entries to evict pub evict_count: usize, /// Whether to enable speculative decoding pub enable_speculative: bool, /// Token budgets for requests pub token_budgets: Vec, /// Quantization recommendation pub quantization_recommendation: Option, /// Estimated latency for current batch pub estimated_latency_ms: f32, /// Estimated throughput for current batch pub estimated_tps: f32, /// Confidence in this decision (0.0 - 1.0) pub confidence: f32, /// Reason for this decision pub reason: String, } impl Default for OptimizationDecision { fn default() -> Self { Self { batch_size: 1, should_evict_kv_cache: false, evict_count: 0, enable_speculative: false, token_budgets: Vec::new(), quantization_recommendation: None, estimated_latency_ms: 0.0, estimated_tps: 0.0, confidence: 0.5, reason: "Default decision".to_string(), } } } /// Real-time optimizer for LLM inference pub struct RealtimeOptimizer { /// Configuration config: RwLock, /// Current batch size current_batch_size: AtomicUsize, /// Current KV cache pressure (0.0 - 1.0) kv_cache_pressure: RwLock, /// Recent latency measurements recent_latencies: RwLock>, /// Recent throughput measurements recent_throughputs: RwLock>, /// Whether speculative decoding is active speculative_active: AtomicBool, /// Draft model identifier (if loaded) draft_model: RwLock>, /// Last optimization time last_optimization: RwLock, /// Pending requests pending_requests: RwLock>, /// Current memory usage current_memory_bytes: AtomicUsize, } impl RealtimeOptimizer { /// Create a new realtime optimizer pub fn new(config: RealtimeConfig) -> Self { let initial_batch_size = match config.batch_strategy { BatchSizeStrategy::Fixed => config.max_batch_size, BatchSizeStrategy::Aggressive => config.max_batch_size, BatchSizeStrategy::Conservative => config.min_batch_size, _ => (config.min_batch_size + config.max_batch_size) / 2, }; Self { config: RwLock::new(config), current_batch_size: AtomicUsize::new(initial_batch_size), kv_cache_pressure: RwLock::new(0.0), recent_latencies: RwLock::new(VecDeque::with_capacity(100)), recent_throughputs: RwLock::new(VecDeque::with_capacity(100)), speculative_active: AtomicBool::new(false), draft_model: RwLock::new(None), last_optimization: RwLock::new(Instant::now()), pending_requests: RwLock::new(Vec::new()), current_memory_bytes: AtomicUsize::new(0), } } /// Optimize batch size based on recent latency measurements pub fn optimize_batch_size(&self, recent_latencies: &[f32]) -> usize { let config = self.config.read(); // Update internal latency tracking { let mut latencies = self.recent_latencies.write(); for &l in recent_latencies { if latencies.len() >= 100 { latencies.pop_front(); } latencies.push_back(l); } } let current_batch = self.current_batch_size.load(Ordering::Relaxed); let new_batch_size = match config.batch_strategy { BatchSizeStrategy::Fixed => current_batch, BatchSizeStrategy::Adaptive => self.adaptive_batch_size(&config, recent_latencies), BatchSizeStrategy::Aggressive => { // Maximize batch size while staying under latency target let avg_latency = self.average_latency(); if avg_latency < config.latency_target_ms * 0.7 { (current_batch + 4).min(config.max_batch_size) } else if avg_latency > config.latency_target_ms { (current_batch.saturating_sub(2)).max(config.min_batch_size) } else { current_batch } } BatchSizeStrategy::Conservative => { // Minimize latency, slowly increase batch size let avg_latency = self.average_latency(); if avg_latency < config.latency_target_ms * 0.5 { (current_batch + 1).min(config.max_batch_size) } else if avg_latency > config.latency_target_ms * 0.8 { (current_batch.saturating_sub(1)).max(config.min_batch_size) } else { current_batch } } BatchSizeStrategy::Hybrid => { // Balance throughput and latency using a utility function self.hybrid_batch_size(&config) } }; self.current_batch_size .store(new_batch_size, Ordering::Relaxed); new_batch_size } /// Adaptive batch sizing based on PID-like control fn adaptive_batch_size(&self, config: &RealtimeConfig, recent_latencies: &[f32]) -> usize { let current_batch = self.current_batch_size.load(Ordering::Relaxed); if recent_latencies.is_empty() { return current_batch; } let avg_latency: f32 = recent_latencies.iter().sum::() / recent_latencies.len() as f32; let target = config.latency_target_ms; // Error term (positive = too slow, negative = too fast) let error = avg_latency - target; let error_ratio = error / target; // PID-like adjustment let adjustment = if error_ratio.abs() < 0.1 { // Within 10% of target, no change 0 } else if error_ratio > 0.0 { // Too slow, reduce batch size let reduction = (error_ratio * 4.0).ceil() as i32; -reduction.min(4) } else { // Too fast, increase batch size let increase = (-error_ratio * 2.0).ceil() as i32; increase.min(2) }; let new_batch = (current_batch as i32 + adjustment) .max(config.min_batch_size as i32) .min(config.max_batch_size as i32) as usize; new_batch } /// Hybrid batch sizing using utility maximization fn hybrid_batch_size(&self, config: &RealtimeConfig) -> usize { let current_batch = self.current_batch_size.load(Ordering::Relaxed); let avg_latency = self.average_latency(); let avg_throughput = self.average_throughput(); // Utility = alpha * throughput_normalized - beta * latency_normalized let alpha = 0.6; // Weight for throughput let beta = 0.4; // Weight for latency let latency_normalized = (avg_latency / config.latency_target_ms).min(2.0); let throughput_normalized = (avg_throughput / config.throughput_target_tps).min(2.0); let current_utility = alpha * throughput_normalized - beta * latency_normalized; // Try neighboring batch sizes and pick the one with best predicted utility let candidates = [ current_batch.saturating_sub(2), current_batch.saturating_sub(1), current_batch, current_batch + 1, current_batch + 2, ]; let mut best_batch = current_batch; let mut best_utility = current_utility; for &candidate in &candidates { if candidate < config.min_batch_size || candidate > config.max_batch_size { continue; } // Predict utility for this batch size let batch_ratio = candidate as f32 / current_batch as f32; let predicted_latency = avg_latency * batch_ratio.sqrt(); // Latency grows sub-linearly let predicted_throughput = avg_throughput * batch_ratio; // Throughput grows linearly let pred_latency_norm = (predicted_latency / config.latency_target_ms).min(2.0); let pred_throughput_norm = (predicted_throughput / config.throughput_target_tps).min(2.0); let predicted_utility = alpha * pred_throughput_norm - beta * pred_latency_norm; if predicted_utility > best_utility { best_utility = predicted_utility; best_batch = candidate; } } best_batch } /// Check if KV cache eviction is needed pub fn should_evict_kv_cache(&self) -> bool { let config = self.config.read(); let pressure = *self.kv_cache_pressure.read(); pressure >= config.kv_cache_pressure_threshold } /// Update KV cache pressure pub fn update_kv_cache_pressure(&self, pressure: f32) { *self.kv_cache_pressure.write() = pressure.clamp(0.0, 1.0); } /// Get KV cache pressure pub fn kv_cache_pressure(&self) -> f32 { *self.kv_cache_pressure.read() } /// Allocate token budgets for a set of requests pub fn allocate_token_budget(&self, requests: &[Request]) -> Vec { let config = self.config.read(); let batch_size = self.current_batch_size.load(Ordering::Relaxed); let memory_budget = config.max_memory_bytes; // Sort requests by priority and deadline let mut sorted_requests: Vec<(usize, &Request)> = requests.iter().enumerate().collect(); sorted_requests.sort_by(|(_, a), (_, b)| { // Higher priority first let priority_cmp = b .priority .partial_cmp(&a.priority) .unwrap_or(std::cmp::Ordering::Equal); if priority_cmp != std::cmp::Ordering::Equal { return priority_cmp; } // Earlier deadline first match (&a.deadline, &b.deadline) { (Some(da), Some(db)) => da.cmp(db), (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => std::cmp::Ordering::Equal, } }); let mut allocations = Vec::with_capacity(requests.len()); let mut total_memory = 0usize; let mut assigned_slots = 0usize; for (original_idx, request) in sorted_requests { // Estimate memory for this request let estimated_memory = self.estimate_request_memory(request); let (max_tokens, batch_slot) = if assigned_slots < batch_size && total_memory + estimated_memory <= memory_budget { total_memory += estimated_memory; let slot = assigned_slots; assigned_slots += 1; (request.max_output_tokens, Some(slot)) } else { // Request is queued, reduced token budget let reduced = (request.max_output_tokens / 2).max(1); (reduced, None) }; let estimated_completion = self.estimate_completion_time(request, batch_slot); allocations.push(( original_idx, TokenBudgetAllocation { request_id: request.id.clone(), max_tokens, priority: request.priority, deadline: request.deadline, batch_slot, estimated_completion_ms: estimated_completion, }, )); } // Sort back to original order allocations.sort_by_key(|(idx, _)| *idx); allocations.into_iter().map(|(_, alloc)| alloc).collect() } /// Estimate memory requirement for a request fn estimate_request_memory(&self, request: &Request) -> usize { // Rough estimate: 2 bytes per token for KV cache (FP16) // Plus overhead for attention computation let kv_memory = (request.input_tokens + request.max_output_tokens) * 2 * 128; // head_dim let attention_overhead = request.input_tokens * 32; // Attention scores kv_memory + attention_overhead } /// Estimate completion time for a request fn estimate_completion_time(&self, request: &Request, batch_slot: Option) -> f32 { let avg_tps = self.average_throughput().max(1.0); let base_time = request.max_output_tokens as f32 / avg_tps * 1000.0; // Add queue time if not in current batch if batch_slot.is_none() { let queue_size = self.pending_requests.read().len(); base_time + (queue_size as f32 * self.average_latency()) } else { base_time } } /// Enable speculative decoding pub fn enable_speculative_decoding(&self, draft_model: &str) { *self.draft_model.write() = Some(draft_model.to_string()); self.speculative_active.store(true, Ordering::Relaxed); } /// Disable speculative decoding pub fn disable_speculative_decoding(&self) { self.speculative_active.store(false, Ordering::Relaxed); } /// Update speculation statistics for learning/monitoring /// /// This records the acceptance rate of speculative decoding rounds /// to help tune the lookahead parameter adaptively. /// /// # Arguments /// * `accepted_count` - Number of draft tokens that were accepted /// * `total_drafted` - Total number of draft tokens generated pub fn update_speculation_stats(&self, accepted_count: usize, total_drafted: usize) { if total_drafted == 0 { return; } // Calculate acceptance rate let acceptance_rate = accepted_count as f32 / total_drafted as f32; // Use acceptance rate to adjust future speculative decoding behavior // High acceptance (>0.8) suggests we can increase lookahead // Low acceptance (<0.5) suggests we should reduce lookahead or disable let mut config = self.config.write(); if acceptance_rate > 0.9 && config.speculative.num_speculative_tokens < 8 { // Excellent acceptance, try more tokens config.speculative.num_speculative_tokens += 1; } else if acceptance_rate < 0.3 && config.speculative.num_speculative_tokens > 2 { // Poor acceptance, reduce speculation config.speculative.num_speculative_tokens -= 1; } // Update acceptance threshold based on recent performance // This implements a simple exponential moving average let alpha = 0.1; // Learning rate config.speculative.acceptance_threshold = config.speculative.acceptance_threshold * (1.0 - alpha) + acceptance_rate * alpha; } /// Check if speculative decoding is active pub fn is_speculative_active(&self) -> bool { self.speculative_active.load(Ordering::Relaxed) } /// Get the draft model identifier pub fn draft_model(&self) -> Option { self.draft_model.read().clone() } /// Record a latency measurement pub fn record_latency(&self, latency_ms: f32) { let mut latencies = self.recent_latencies.write(); if latencies.len() >= 100 { latencies.pop_front(); } latencies.push_back(latency_ms); } /// Record a throughput measurement pub fn record_throughput(&self, tps: f32) { let mut throughputs = self.recent_throughputs.write(); if throughputs.len() >= 100 { throughputs.pop_front(); } throughputs.push_back(tps); } /// Get average latency pub fn average_latency(&self) -> f32 { let latencies = self.recent_latencies.read(); if latencies.is_empty() { return 50.0; // Default estimate } latencies.iter().sum::() / latencies.len() as f32 } /// Get average throughput pub fn average_throughput(&self) -> f32 { let throughputs = self.recent_throughputs.read(); if throughputs.is_empty() { return 50.0; // Default estimate } throughputs.iter().sum::() / throughputs.len() as f32 } /// Update memory usage pub fn update_memory_usage(&self, bytes: usize) { self.current_memory_bytes.store(bytes, Ordering::Relaxed); } /// Get memory pressure (0.0 - 1.0) pub fn memory_pressure(&self) -> f32 { let config = self.config.read(); let current = self.current_memory_bytes.load(Ordering::Relaxed); current as f32 / config.max_memory_bytes as f32 } /// Make a comprehensive optimization decision pub fn optimize(&self, metrics: &InferenceMetrics) -> OptimizationDecision { let config = self.config.read(); let snapshot = metrics.snapshot(); // Check if we need to optimize let last_opt = *self.last_optimization.read(); if last_opt.elapsed().as_millis() < config.optimization_interval_ms as u128 { return OptimizationDecision { batch_size: self.current_batch_size.load(Ordering::Relaxed), confidence: 0.3, reason: "Skipping optimization (too recent)".to_string(), ..Default::default() }; } *self.last_optimization.write() = Instant::now(); // Determine batch size let latencies: Vec = self.recent_latencies.read().iter().copied().collect(); let batch_size = self.optimize_batch_size(&latencies); // Determine KV cache action let kv_pressure = *self.kv_cache_pressure.read(); let (should_evict, evict_count) = if kv_pressure >= config.kv_cache_pressure_threshold { let excess_pressure = kv_pressure - config.kv_cache_pressure_threshold; let evict_ratio = (excess_pressure / (1.0 - config.kv_cache_pressure_threshold)).min(0.5); (true, (evict_ratio * 1000.0) as usize) // Evict proportionally } else { (false, 0) }; // Determine speculative decoding let enable_speculative = config.enable_speculative && snapshot.ttft_avg_ms < config.latency_target_ms * 0.5 && self.draft_model.read().is_some(); // Token budget allocation for pending requests let pending = self.pending_requests.read().clone(); let token_budgets = self.allocate_token_budget(&pending); // Quantization recommendation let quantization_recommendation = if self.memory_pressure() > 0.8 { Some("Q4".to_string()) } else if self.memory_pressure() > 0.6 { Some("Q8".to_string()) } else { None }; // Estimate outcomes let batch_ratio = batch_size as f32 / self.current_batch_size.load(Ordering::Relaxed).max(1) as f32; let estimated_latency = snapshot.ttft_avg_ms * batch_ratio.sqrt(); let estimated_tps = snapshot.tps_avg * batch_ratio; // Calculate confidence based on data quality let sample_count = latencies.len(); let confidence = if sample_count < 10 { 0.3 } else if sample_count < 50 { 0.6 } else { 0.9 }; // Generate reason let reason = format!( "Batch: {} (latency={:.1}ms, target={:.1}ms), KV pressure: {:.1}%, Memory: {:.1}%", batch_size, snapshot.ttft_avg_ms, config.latency_target_ms, kv_pressure * 100.0, self.memory_pressure() * 100.0 ); OptimizationDecision { batch_size, should_evict_kv_cache: should_evict, evict_count, enable_speculative, token_budgets, quantization_recommendation, estimated_latency_ms: estimated_latency, estimated_tps, confidence, reason, } } /// Add a pending request pub fn add_request(&self, request: Request) { self.pending_requests.write().push(request); } /// Remove a completed request pub fn remove_request(&self, request_id: &str) { self.pending_requests.write().retain(|r| r.id != request_id); } /// Get pending request count pub fn pending_request_count(&self) -> usize { self.pending_requests.read().len() } /// Get current batch size pub fn current_batch_size(&self) -> usize { self.current_batch_size.load(Ordering::Relaxed) } /// Update configuration pub fn update_config(&self, config: RealtimeConfig) { *self.config.write() = config; } /// Get current configuration pub fn config(&self) -> RealtimeConfig { self.config.read().clone() } } impl RealtimeOptimizer { /// Check if speculative decoding should be used for these generation parameters /// /// Returns true when: /// - Temperature is low (< 0.5) - deterministic generation benefits most /// - Greedy decoding (top_k = 1) /// - Speculative decoding is enabled in config pub fn should_use_speculative(&self, params: &crate::backends::GenerateParams) -> bool { let config = self.config.read(); if !config.enable_speculative { return false; } // Speculative decoding is most effective for: // 1. Low temperature (more deterministic) // 2. Greedy decoding // 3. When not using high top-p sampling params.temperature < 0.5 || params.top_k == 1 } /// Get recommended speculative decoding configuration based on current metrics pub fn get_speculative_config(&self) -> SpeculativeConfig { let config = self.config.read(); let avg_latency = self.average_latency(); let memory_pressure = self.memory_pressure(); // Adjust speculative config based on system state let mut spec_config = config.speculative.clone(); // Reduce lookahead under memory pressure if memory_pressure > 0.8 { spec_config.num_speculative_tokens = (spec_config.num_speculative_tokens / 2).max(2); } // Increase acceptance threshold when latency is high if avg_latency > config.latency_target_ms { spec_config.acceptance_threshold = (spec_config.acceptance_threshold + 0.1).min(0.95); } spec_config } } impl Default for RealtimeOptimizer { fn default() -> Self { Self::new(RealtimeConfig::default()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_realtime_config_default() { let config = RealtimeConfig::default(); assert!((config.latency_target_ms - 100.0).abs() < 0.01); assert!((config.throughput_target_tps - 50.0).abs() < 0.01); } #[test] fn test_optimizer_creation() { let config = RealtimeConfig { min_batch_size: 1, max_batch_size: 32, batch_strategy: BatchSizeStrategy::Adaptive, ..Default::default() }; let optimizer = RealtimeOptimizer::new(config); assert!(optimizer.current_batch_size() >= 1); assert!(optimizer.current_batch_size() <= 32); } #[test] fn test_batch_size_optimization() { let config = RealtimeConfig { latency_target_ms: 100.0, min_batch_size: 1, max_batch_size: 16, batch_strategy: BatchSizeStrategy::Adaptive, ..Default::default() }; let optimizer = RealtimeOptimizer::new(config); // High latency should reduce batch size let high_latencies = vec![150.0, 160.0, 140.0]; let batch = optimizer.optimize_batch_size(&high_latencies); assert!(batch <= 8, "High latency should reduce batch size"); // Low latency should increase batch size let low_latencies = vec![30.0, 35.0, 25.0]; let batch = optimizer.optimize_batch_size(&low_latencies); assert!(batch >= 4, "Low latency should allow larger batch size"); } #[test] fn test_kv_cache_pressure() { let config = RealtimeConfig { kv_cache_pressure_threshold: 0.8, ..Default::default() }; let optimizer = RealtimeOptimizer::new(config); optimizer.update_kv_cache_pressure(0.5); assert!(!optimizer.should_evict_kv_cache()); optimizer.update_kv_cache_pressure(0.9); assert!(optimizer.should_evict_kv_cache()); } #[test] fn test_token_budget_allocation() { let optimizer = RealtimeOptimizer::new(RealtimeConfig::default()); let requests = vec![ Request { id: "req1".to_string(), input_tokens: 100, max_output_tokens: 200, priority: 0.9, arrival_time: Instant::now(), deadline: None, }, Request { id: "req2".to_string(), input_tokens: 50, max_output_tokens: 100, priority: 0.5, arrival_time: Instant::now(), deadline: Some(Duration::from_secs(1)), }, ]; let allocations = optimizer.allocate_token_budget(&requests); assert_eq!(allocations.len(), 2); // Higher priority request should get more resources let high_priority = allocations.iter().find(|a| a.request_id == "req1").unwrap(); assert!(high_priority.batch_slot.is_some() || high_priority.max_tokens >= 100); } #[test] fn test_speculative_decoding() { let optimizer = RealtimeOptimizer::new(RealtimeConfig { enable_speculative: true, ..Default::default() }); assert!(!optimizer.is_speculative_active()); optimizer.enable_speculative_decoding("draft-model-1"); assert!(optimizer.is_speculative_active()); assert_eq!(optimizer.draft_model(), Some("draft-model-1".to_string())); optimizer.disable_speculative_decoding(); assert!(!optimizer.is_speculative_active()); } #[test] fn test_optimization_decision() { let optimizer = RealtimeOptimizer::new(RealtimeConfig::default()); let metrics = InferenceMetrics::new(); // Record some metrics for i in 1..=10 { metrics.record_ttft(i as f32 * 10.0); optimizer.record_latency(i as f32 * 10.0); optimizer.record_throughput(50.0 + i as f32); } let decision = optimizer.optimize(&metrics); assert!(decision.batch_size >= 1); assert!(decision.confidence > 0.0); } #[test] fn test_memory_pressure() { let config = RealtimeConfig { max_memory_bytes: 1024 * 1024 * 1024, // 1GB ..Default::default() }; let optimizer = RealtimeOptimizer::new(config); optimizer.update_memory_usage(512 * 1024 * 1024); // 512MB assert!((optimizer.memory_pressure() - 0.5).abs() < 0.01); optimizer.update_memory_usage(800 * 1024 * 1024); // 800MB assert!((optimizer.memory_pressure() - 0.78).abs() < 0.02); } #[test] fn test_batch_strategies() { let strategies = vec![ BatchSizeStrategy::Fixed, BatchSizeStrategy::Adaptive, BatchSizeStrategy::Aggressive, BatchSizeStrategy::Conservative, BatchSizeStrategy::Hybrid, ]; for strategy in strategies { let config = RealtimeConfig { batch_strategy: strategy, min_batch_size: 1, max_batch_size: 16, ..Default::default() }; let optimizer = RealtimeOptimizer::new(config); let latencies = vec![50.0, 55.0, 45.0]; let batch = optimizer.optimize_batch_size(&latencies); assert!( batch >= 1 && batch <= 16, "Strategy {:?} produced invalid batch size", strategy ); } } }