Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions
@@ -0,0 +1,821 @@
//! Inference Metrics for Optimization Decisions
//!
//! This module provides comprehensive metrics collection for LLM inference,
//! enabling data-driven optimization decisions.
//!
//! ## Tracked Metrics
//!
//! - **TTFT (Time to First Token)**: Latency until first token generation
//! - **TPS (Tokens Per Second)**: Generation throughput
//! - **KV Cache Hit Rate**: Cache efficiency metric
//! - **Memory Usage**: Current memory consumption
//! - **Request Statistics**: Active requests, queue depth
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
/// Moving average calculator with configurable window
#[derive(Debug)]
pub struct MovingAverage {
/// Circular buffer of values
values: RwLock<VecDeque<f32>>,
/// Window size
window_size: usize,
/// Running sum for O(1) average calculation
running_sum: RwLock<f32>,
}
impl MovingAverage {
/// Create a new moving average calculator
pub fn new(window_size: usize) -> Self {
Self {
values: RwLock::new(VecDeque::with_capacity(window_size)),
window_size,
running_sum: RwLock::new(0.0),
}
}
/// Add a value to the moving average
pub fn add(&self, value: f32) {
let mut values = self.values.write();
let mut sum = self.running_sum.write();
// Remove oldest if at capacity
if values.len() >= self.window_size {
if let Some(old) = values.pop_front() {
*sum -= old;
}
}
values.push_back(value);
*sum += value;
}
/// Get the current average
pub fn average(&self) -> f32 {
let values = self.values.read();
let sum = self.running_sum.read();
if values.is_empty() {
0.0
} else {
*sum / values.len() as f32
}
}
/// Get the minimum value in the window
pub fn min(&self) -> f32 {
let values = self.values.read();
values.iter().cloned().fold(f32::INFINITY, f32::min)
}
/// Get the maximum value in the window
pub fn max(&self) -> f32 {
let values = self.values.read();
values.iter().cloned().fold(f32::NEG_INFINITY, f32::max)
}
/// Get the standard deviation
pub fn std_dev(&self) -> f32 {
let values = self.values.read();
if values.len() < 2 {
return 0.0;
}
let mean = self.average();
let variance: f32 =
values.iter().map(|v| (v - mean).powi(2)).sum::<f32>() / (values.len() - 1) as f32;
variance.sqrt()
}
/// Get the current window size (number of samples)
pub fn count(&self) -> usize {
self.values.read().len()
}
/// Clear all values
pub fn clear(&self) {
let mut values = self.values.write();
let mut sum = self.running_sum.write();
values.clear();
*sum = 0.0;
}
/// Get percentile value (0-100)
pub fn percentile(&self, p: f32) -> f32 {
let values = self.values.read();
if values.is_empty() {
return 0.0;
}
let mut sorted: Vec<f32> = values.iter().copied().collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((p / 100.0) * (sorted.len() - 1) as f32).round() as usize;
sorted[idx.min(sorted.len() - 1)]
}
}
impl Default for MovingAverage {
fn default() -> Self {
Self::new(100)
}
}
impl Clone for MovingAverage {
fn clone(&self) -> Self {
let values = self.values.read();
let sum = self.running_sum.read();
Self {
values: RwLock::new(values.clone()),
window_size: self.window_size,
running_sum: RwLock::new(*sum),
}
}
}
/// Latency histogram for distribution analysis
#[derive(Debug)]
pub struct LatencyHistogram {
/// Bucket boundaries in milliseconds
buckets: Vec<f32>,
/// Counts per bucket
counts: Vec<AtomicU64>,
/// Total count
total: AtomicU64,
/// Sum for mean calculation
sum: RwLock<f64>,
}
impl LatencyHistogram {
/// Create a new histogram with default buckets
pub fn new() -> Self {
Self::with_buckets(vec![
1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0,
])
}
/// Create a histogram with custom bucket boundaries
pub fn with_buckets(buckets: Vec<f32>) -> Self {
let counts = buckets.iter().map(|_| AtomicU64::new(0)).collect();
Self {
buckets,
counts,
total: AtomicU64::new(0),
sum: RwLock::new(0.0),
}
}
/// Record a latency value in milliseconds
pub fn record(&self, latency_ms: f32) {
// Find the appropriate bucket
let bucket_idx = self
.buckets
.iter()
.position(|&b| latency_ms <= b)
.unwrap_or(self.buckets.len() - 1);
self.counts[bucket_idx].fetch_add(1, Ordering::Relaxed);
self.total.fetch_add(1, Ordering::Relaxed);
let mut sum = self.sum.write();
*sum += latency_ms as f64;
}
/// Get the mean latency
pub fn mean(&self) -> f32 {
let total = self.total.load(Ordering::Relaxed);
if total == 0 {
return 0.0;
}
let sum = self.sum.read();
(*sum / total as f64) as f32
}
/// Get approximate percentile (linear interpolation between buckets)
pub fn percentile(&self, p: f32) -> f32 {
let total = self.total.load(Ordering::Relaxed);
if total == 0 {
return 0.0;
}
let target = (p / 100.0 * total as f32) as u64;
let mut cumulative = 0u64;
for (i, count) in self.counts.iter().enumerate() {
let bucket_count = count.load(Ordering::Relaxed);
cumulative += bucket_count;
if cumulative >= target {
// Found the bucket containing the percentile
if i == 0 {
return self.buckets[0];
}
// Linear interpolation
let prev_cumulative = cumulative - bucket_count;
let fraction = (target - prev_cumulative) as f32 / bucket_count.max(1) as f32;
let prev_bucket = if i > 0 { self.buckets[i - 1] } else { 0.0 };
return prev_bucket + fraction * (self.buckets[i] - prev_bucket);
}
}
*self.buckets.last().unwrap_or(&0.0)
}
/// Get bucket counts for visualization
pub fn bucket_counts(&self) -> Vec<(f32, u64)> {
self.buckets
.iter()
.zip(self.counts.iter())
.map(|(b, c)| (*b, c.load(Ordering::Relaxed)))
.collect()
}
/// Reset all counts
pub fn reset(&self) {
for count in &self.counts {
count.store(0, Ordering::Relaxed);
}
self.total.store(0, Ordering::Relaxed);
*self.sum.write() = 0.0;
}
/// Get total count
pub fn count(&self) -> u64 {
self.total.load(Ordering::Relaxed)
}
}
impl Default for LatencyHistogram {
fn default() -> Self {
Self::new()
}
}
impl Clone for LatencyHistogram {
fn clone(&self) -> Self {
let counts: Vec<AtomicU64> = self
.counts
.iter()
.map(|c| AtomicU64::new(c.load(Ordering::Relaxed)))
.collect();
let sum = *self.sum.read();
Self {
buckets: self.buckets.clone(),
counts,
total: AtomicU64::new(self.total.load(Ordering::Relaxed)),
sum: RwLock::new(sum),
}
}
}
/// Comprehensive inference metrics
#[derive(Debug)]
pub struct InferenceMetrics {
/// Time to first token (milliseconds)
pub ttft_ms: MovingAverage,
/// Tokens per second throughput
pub tps: MovingAverage,
/// KV cache hit rate (0.0 - 1.0)
kv_cache_hits: AtomicU64,
kv_cache_misses: AtomicU64,
/// Memory usage in bytes
memory_usage_bytes: AtomicUsize,
/// Peak memory usage
peak_memory_bytes: AtomicUsize,
/// Active request count
active_requests: AtomicUsize,
/// Total requests processed
total_requests: AtomicU64,
/// Total tokens generated
total_tokens: AtomicU64,
/// Request latency histogram
pub latency_histogram: LatencyHistogram,
/// Queue depth for pending requests
queue_depth: AtomicUsize,
/// Start time for uptime calculation
start_time: Instant,
/// Last update time
last_update: RwLock<Instant>,
/// Inter-token latency
pub inter_token_latency_ms: MovingAverage,
/// Batch size history
pub batch_sizes: MovingAverage,
}
impl InferenceMetrics {
/// Create new inference metrics
pub fn new() -> Self {
Self {
ttft_ms: MovingAverage::new(100),
tps: MovingAverage::new(100),
kv_cache_hits: AtomicU64::new(0),
kv_cache_misses: AtomicU64::new(0),
memory_usage_bytes: AtomicUsize::new(0),
peak_memory_bytes: AtomicUsize::new(0),
active_requests: AtomicUsize::new(0),
total_requests: AtomicU64::new(0),
total_tokens: AtomicU64::new(0),
latency_histogram: LatencyHistogram::new(),
queue_depth: AtomicUsize::new(0),
start_time: Instant::now(),
last_update: RwLock::new(Instant::now()),
inter_token_latency_ms: MovingAverage::new(100),
batch_sizes: MovingAverage::new(50),
}
}
/// Record time to first token
pub fn record_ttft(&self, ttft_ms: f32) {
self.ttft_ms.add(ttft_ms);
self.latency_histogram.record(ttft_ms);
*self.last_update.write() = Instant::now();
}
/// Record tokens per second for a generation
pub fn record_tps(&self, tokens: usize, duration: Duration) {
if duration.as_secs_f32() > 0.0 {
let tps = tokens as f32 / duration.as_secs_f32();
self.tps.add(tps);
}
self.total_tokens
.fetch_add(tokens as u64, Ordering::Relaxed);
*self.last_update.write() = Instant::now();
}
/// Record inter-token latency
pub fn record_inter_token_latency(&self, latency_ms: f32) {
self.inter_token_latency_ms.add(latency_ms);
}
/// Record batch size
pub fn record_batch_size(&self, size: usize) {
self.batch_sizes.add(size as f32);
}
/// Record KV cache hit
pub fn record_kv_cache_hit(&self) {
self.kv_cache_hits.fetch_add(1, Ordering::Relaxed);
}
/// Record KV cache miss
pub fn record_kv_cache_miss(&self) {
self.kv_cache_misses.fetch_add(1, Ordering::Relaxed);
}
/// Get current KV cache hit rate
pub fn kv_cache_hit_rate(&self) -> f32 {
let hits = self.kv_cache_hits.load(Ordering::Relaxed);
let misses = self.kv_cache_misses.load(Ordering::Relaxed);
let total = hits + misses;
if total == 0 {
1.0 // No accesses yet, assume perfect
} else {
hits as f32 / total as f32
}
}
/// Update memory usage
pub fn update_memory_usage(&self, bytes: usize) {
self.memory_usage_bytes.store(bytes, Ordering::Relaxed);
// Update peak if necessary
let current_peak = self.peak_memory_bytes.load(Ordering::Relaxed);
if bytes > current_peak {
self.peak_memory_bytes.store(bytes, Ordering::Relaxed);
}
}
/// Get current memory usage
pub fn memory_usage_bytes(&self) -> usize {
self.memory_usage_bytes.load(Ordering::Relaxed)
}
/// Get peak memory usage
pub fn peak_memory_bytes(&self) -> usize {
self.peak_memory_bytes.load(Ordering::Relaxed)
}
/// Increment active requests
pub fn request_started(&self) {
self.active_requests.fetch_add(1, Ordering::Relaxed);
self.total_requests.fetch_add(1, Ordering::Relaxed);
}
/// Decrement active requests
pub fn request_completed(&self) {
self.active_requests.fetch_sub(1, Ordering::Relaxed);
}
/// Get active request count
pub fn active_requests(&self) -> usize {
self.active_requests.load(Ordering::Relaxed)
}
/// Get total requests
pub fn total_requests(&self) -> u64 {
self.total_requests.load(Ordering::Relaxed)
}
/// Get total tokens generated
pub fn total_tokens(&self) -> u64 {
self.total_tokens.load(Ordering::Relaxed)
}
/// Update queue depth
pub fn set_queue_depth(&self, depth: usize) {
self.queue_depth.store(depth, Ordering::Relaxed);
}
/// Get queue depth
pub fn queue_depth(&self) -> usize {
self.queue_depth.load(Ordering::Relaxed)
}
/// Get uptime duration
pub fn uptime(&self) -> Duration {
self.start_time.elapsed()
}
/// Get time since last update
pub fn time_since_update(&self) -> Duration {
self.last_update.read().elapsed()
}
/// Take a snapshot of current metrics
pub fn snapshot(&self) -> MetricsSnapshot {
MetricsSnapshot {
ttft_avg_ms: self.ttft_ms.average(),
ttft_p50_ms: self.ttft_ms.percentile(50.0),
ttft_p95_ms: self.ttft_ms.percentile(95.0),
ttft_p99_ms: self.ttft_ms.percentile(99.0),
tps_avg: self.tps.average(),
tps_min: self.tps.min(),
tps_max: self.tps.max(),
kv_cache_hit_rate: self.kv_cache_hit_rate(),
memory_usage_bytes: self.memory_usage_bytes(),
peak_memory_bytes: self.peak_memory_bytes(),
active_requests: self.active_requests(),
total_requests: self.total_requests(),
total_tokens: self.total_tokens(),
queue_depth: self.queue_depth(),
uptime_secs: self.uptime().as_secs_f32(),
inter_token_latency_avg_ms: self.inter_token_latency_ms.average(),
avg_batch_size: self.batch_sizes.average(),
}
}
/// Reset all metrics
pub fn reset(&self) {
self.ttft_ms.clear();
self.tps.clear();
self.kv_cache_hits.store(0, Ordering::Relaxed);
self.kv_cache_misses.store(0, Ordering::Relaxed);
self.peak_memory_bytes.store(
self.memory_usage_bytes.load(Ordering::Relaxed),
Ordering::Relaxed,
);
self.total_requests.store(0, Ordering::Relaxed);
self.total_tokens.store(0, Ordering::Relaxed);
self.latency_histogram.reset();
self.inter_token_latency_ms.clear();
self.batch_sizes.clear();
}
}
impl Default for InferenceMetrics {
fn default() -> Self {
Self::new()
}
}
/// Snapshot of metrics at a point in time
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsSnapshot {
/// Average time to first token (ms)
pub ttft_avg_ms: f32,
/// P50 TTFT
pub ttft_p50_ms: f32,
/// P95 TTFT
pub ttft_p95_ms: f32,
/// P99 TTFT
pub ttft_p99_ms: f32,
/// Average tokens per second
pub tps_avg: f32,
/// Minimum TPS observed
pub tps_min: f32,
/// Maximum TPS observed
pub tps_max: f32,
/// KV cache hit rate (0.0 - 1.0)
pub kv_cache_hit_rate: f32,
/// Current memory usage (bytes)
pub memory_usage_bytes: usize,
/// Peak memory usage (bytes)
pub peak_memory_bytes: usize,
/// Active requests
pub active_requests: usize,
/// Total requests processed
pub total_requests: u64,
/// Total tokens generated
pub total_tokens: u64,
/// Queue depth
pub queue_depth: usize,
/// Uptime in seconds
pub uptime_secs: f32,
/// Average inter-token latency
pub inter_token_latency_avg_ms: f32,
/// Average batch size
pub avg_batch_size: f32,
}
impl MetricsSnapshot {
/// Check if metrics indicate healthy performance
pub fn is_healthy(&self, max_ttft_ms: f32, min_tps: f32) -> bool {
self.ttft_avg_ms <= max_ttft_ms && self.tps_avg >= min_tps
}
/// Calculate throughput efficiency
pub fn throughput_efficiency(&self, target_tps: f32) -> f32 {
if target_tps <= 0.0 {
return 1.0;
}
(self.tps_avg / target_tps).min(1.0)
}
/// Calculate latency score (0-1, higher is better)
pub fn latency_score(&self, target_ttft_ms: f32) -> f32 {
if self.ttft_avg_ms <= 0.0 {
return 1.0;
}
(target_ttft_ms / self.ttft_avg_ms).min(1.0)
}
}
/// Metrics collector with periodic aggregation
pub struct MetricsCollector {
/// Current metrics
metrics: InferenceMetrics,
/// Historical snapshots
history: RwLock<VecDeque<(Instant, MetricsSnapshot)>>,
/// Maximum history size
max_history: usize,
/// Snapshot interval
snapshot_interval: Duration,
/// Last snapshot time
last_snapshot: RwLock<Instant>,
}
impl MetricsCollector {
/// Create a new metrics collector
pub fn new(max_history: usize, snapshot_interval: Duration) -> Self {
Self {
metrics: InferenceMetrics::new(),
history: RwLock::new(VecDeque::with_capacity(max_history)),
max_history,
snapshot_interval,
last_snapshot: RwLock::new(Instant::now()),
}
}
/// Get reference to current metrics
pub fn metrics(&self) -> &InferenceMetrics {
&self.metrics
}
/// Record TTFT and auto-snapshot if needed
pub fn record_ttft(&self, ttft_ms: f32) {
self.metrics.record_ttft(ttft_ms);
self.maybe_snapshot();
}
/// Record TPS and auto-snapshot if needed
pub fn record_tps(&self, tokens: usize, duration: Duration) {
self.metrics.record_tps(tokens, duration);
self.maybe_snapshot();
}
/// Check if snapshot is needed and take it
fn maybe_snapshot(&self) {
let last = *self.last_snapshot.read();
if last.elapsed() >= self.snapshot_interval {
self.take_snapshot();
}
}
/// Force a snapshot
pub fn take_snapshot(&self) {
let snapshot = self.metrics.snapshot();
let now = Instant::now();
let mut history = self.history.write();
if history.len() >= self.max_history {
history.pop_front();
}
history.push_back((now, snapshot));
*self.last_snapshot.write() = now;
}
/// Get recent snapshots
pub fn get_history(&self, count: usize) -> Vec<MetricsSnapshot> {
let history = self.history.read();
history
.iter()
.rev()
.take(count)
.map(|(_, s)| s.clone())
.collect()
}
/// Get trend analysis (positive = improving, negative = degrading)
pub fn ttft_trend(&self) -> f32 {
let history = self.history.read();
if history.len() < 2 {
return 0.0;
}
let recent: Vec<f32> = history
.iter()
.rev()
.take(10)
.map(|(_, s)| s.ttft_avg_ms)
.collect();
if recent.len() < 2 {
return 0.0;
}
// Simple linear regression slope
let n = recent.len() as f32;
let sum_x: f32 = (0..recent.len()).map(|i| i as f32).sum();
let sum_y: f32 = recent.iter().sum();
let sum_xy: f32 = recent.iter().enumerate().map(|(i, y)| i as f32 * y).sum();
let sum_xx: f32 = (0..recent.len()).map(|i| (i * i) as f32).sum();
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
// Negative slope means TTFT is decreasing (improving)
-slope
}
/// Get TPS trend
pub fn tps_trend(&self) -> f32 {
let history = self.history.read();
if history.len() < 2 {
return 0.0;
}
let recent: Vec<f32> = history
.iter()
.rev()
.take(10)
.map(|(_, s)| s.tps_avg)
.collect();
if recent.len() < 2 {
return 0.0;
}
let n = recent.len() as f32;
let sum_x: f32 = (0..recent.len()).map(|i| i as f32).sum();
let sum_y: f32 = recent.iter().sum();
let sum_xy: f32 = recent.iter().enumerate().map(|(i, y)| i as f32 * y).sum();
let sum_xx: f32 = (0..recent.len()).map(|i| (i * i) as f32).sum();
(n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x)
}
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new(1000, Duration::from_secs(60))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_moving_average() {
let ma = MovingAverage::new(3);
ma.add(1.0);
ma.add(2.0);
ma.add(3.0);
assert!((ma.average() - 2.0).abs() < 0.01);
// Adding 4th value should evict 1.0
ma.add(4.0);
assert!((ma.average() - 3.0).abs() < 0.01);
}
#[test]
fn test_moving_average_percentile() {
let ma = MovingAverage::new(10);
for i in 1..=10 {
ma.add(i as f32);
}
let p50 = ma.percentile(50.0);
assert!(p50 >= 5.0 && p50 <= 6.0);
let p90 = ma.percentile(90.0);
assert!(p90 >= 9.0);
}
#[test]
fn test_latency_histogram() {
let hist = LatencyHistogram::new();
hist.record(5.0);
hist.record(15.0);
hist.record(50.0);
assert_eq!(hist.count(), 3);
assert!((hist.mean() - 23.33).abs() < 1.0);
}
#[test]
fn test_inference_metrics() {
let metrics = InferenceMetrics::new();
metrics.record_ttft(10.0);
metrics.record_ttft(20.0);
assert!((metrics.ttft_ms.average() - 15.0).abs() < 0.01);
metrics.record_kv_cache_hit();
metrics.record_kv_cache_hit();
metrics.record_kv_cache_miss();
assert!((metrics.kv_cache_hit_rate() - 0.667).abs() < 0.01);
}
#[test]
fn test_metrics_snapshot() {
let metrics = InferenceMetrics::new();
metrics.record_ttft(10.0);
metrics.record_tps(100, Duration::from_secs(1));
metrics.update_memory_usage(1024 * 1024);
metrics.request_started();
let snapshot = metrics.snapshot();
assert!((snapshot.ttft_avg_ms - 10.0).abs() < 0.01);
assert!((snapshot.tps_avg - 100.0).abs() < 0.01);
assert_eq!(snapshot.memory_usage_bytes, 1024 * 1024);
assert_eq!(snapshot.active_requests, 1);
}
#[test]
fn test_metrics_collector() {
let collector = MetricsCollector::new(100, Duration::from_millis(10));
for i in 1..=5 {
collector.record_ttft(i as f32 * 10.0);
}
collector.take_snapshot();
let history = collector.get_history(1);
assert_eq!(history.len(), 1);
}
#[test]
fn test_snapshot_health_check() {
let snapshot = MetricsSnapshot {
ttft_avg_ms: 50.0,
ttft_p50_ms: 45.0,
ttft_p95_ms: 80.0,
ttft_p99_ms: 100.0,
tps_avg: 150.0,
tps_min: 100.0,
tps_max: 200.0,
kv_cache_hit_rate: 0.95,
memory_usage_bytes: 1024 * 1024,
peak_memory_bytes: 2 * 1024 * 1024,
active_requests: 5,
total_requests: 1000,
total_tokens: 100000,
queue_depth: 2,
uptime_secs: 3600.0,
inter_token_latency_avg_ms: 5.0,
avg_batch_size: 8.0,
};
assert!(snapshot.is_healthy(100.0, 100.0));
assert!(!snapshot.is_healthy(30.0, 100.0)); // TTFT too high
assert!(!snapshot.is_healthy(100.0, 200.0)); // TPS too low
}
}
+112
View File
@@ -0,0 +1,112 @@
//! Real-time Optimization System for RuvLLM
//!
//! This module provides the optimization infrastructure for LLM inference,
//! integrating SONA learning with MicroLoRA and custom kernels.
//!
//! ## Quick Start
//!
//! ```rust,ignore
//! use ruvllm::optimization::{
//! SonaLlm, SonaLlmConfig, RealtimeOptimizer, RealtimeConfig,
//! MetricsCollector, ConsolidationStrategy,
//! };
//!
//! // Create SONA integration for three-tier learning
//! let sona_config = SonaLlmConfig {
//! instant_lr: 0.01,
//! background_interval_ms: 100,
//! background_min_samples: 10,
//! consolidation_strategy: ConsolidationStrategy::EwcMerge,
//! ..Default::default()
//! };
//! let sona = SonaLlm::new(sona_config);
//!
//! // During inference: instant adaptation
//! let result = sona.instant_adapt(&query_embedding, &response_embedding, 0.85);
//! println!("Adapt latency: {}us", result.latency_us);
//!
//! // Periodic: background consolidation
//! if let Some(bg_result) = sona.maybe_background() {
//! println!("Consolidated {} samples", bg_result.samples_used);
//! }
//!
//! // Triggered: deep optimization
//! if sona.should_trigger_deep() {
//! let deep_result = sona.deep_optimize(&samples);
//! println!("Quality delta: {:.3}", deep_result.quality_delta);
//! }
//! ```
//!
//! ## Architecture
//!
//! ```text
//! +-------------------+ +-------------------+
//! | Inference Request |---->| RealtimeOptimizer |
//! | (tokens, params) | | - Batch sizing |
//! +-------------------+ | - KV management |
//! | - Token budgets |
//! +--------+----------+
//! |
//! v (metrics)
//! +--------+----------+
//! | InferenceMetrics |
//! | - TTFT tracking |
//! | - TPS monitoring |
//! | - Memory usage |
//! +--------+----------+
//! |
//! v (feedback)
//! +--------+----------+
//! | SonaLlm |
//! | - Instant adapt |
//! | - Background loop |
//! | - Deep optimize |
//! +-------------------+
//! ```
//!
//! ## SONA Learning Tiers
//!
//! | Tier | Latency | Trigger | Action |
//! |------|---------|---------|--------|
//! | Instant | <1ms | Every request | MicroLoRA gradient update |
//! | Background | ~100ms | Timer/threshold | Pattern consolidation |
//! | Deep | Minutes | Manual/scheduled | Full training pipeline |
//!
//! ## Features
//!
//! - **Real-time Optimization**: Dynamic batch sizing and KV cache management
//! - **SONA Integration**: Three-tier learning loops for continuous improvement
//! - **Metrics Collection**: Comprehensive inference telemetry
//! - **Speculative Decoding**: Draft model integration for faster generation
//!
//! ## Consolidation Strategies
//!
//! ```rust,ignore
//! use ruvllm::optimization::ConsolidationStrategy;
//!
//! // EWC++ merge (default) - preserves important weights
//! let strategy = ConsolidationStrategy::EwcMerge;
//!
//! // Quality-weighted - higher quality samples have more influence
//! let strategy = ConsolidationStrategy::QualityWeighted;
//!
//! // Best only - keep top 20% by quality
//! let strategy = ConsolidationStrategy::BestOnly;
//! ```
pub mod metrics;
pub mod realtime;
pub mod sona_llm;
// Re-exports
pub use metrics::{
InferenceMetrics, LatencyHistogram, MetricsCollector, MetricsSnapshot, MovingAverage,
};
pub use realtime::{
BatchSizeStrategy, KvCachePressurePolicy, OptimizationDecision, RealtimeConfig,
RealtimeOptimizer, SpeculativeConfig, TokenBudgetAllocation,
};
pub use sona_llm::{
AdaptationResult, ConsolidationStrategy, LearningLoopStats, OptimizationTrigger, SonaLlm,
SonaLlmConfig, TrainingSample,
};
@@ -0,0 +1,956 @@
//! 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<String>,
/// 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<Duration>,
/// Allocated batch slot
pub batch_slot: Option<usize>,
/// 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<Duration>,
}
/// 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<TokenBudgetAllocation>,
/// Quantization recommendation
pub quantization_recommendation: Option<String>,
/// 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<RealtimeConfig>,
/// Current batch size
current_batch_size: AtomicUsize,
/// Current KV cache pressure (0.0 - 1.0)
kv_cache_pressure: RwLock<f32>,
/// Recent latency measurements
recent_latencies: RwLock<VecDeque<f32>>,
/// Recent throughput measurements
recent_throughputs: RwLock<VecDeque<f32>>,
/// Whether speculative decoding is active
speculative_active: AtomicBool,
/// Draft model identifier (if loaded)
draft_model: RwLock<Option<String>>,
/// Last optimization time
last_optimization: RwLock<Instant>,
/// Pending requests
pending_requests: RwLock<Vec<Request>>,
/// 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::<f32>() / 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<TokenBudgetAllocation> {
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<usize>) -> 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<String> {
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::<f32>() / 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::<f32>() / 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<f32> = 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
);
}
}
}
File diff suppressed because it is too large Load Diff