use crate::error::{KizzasiError, KizzasiResult};
use crate::predictor::{Kizzasi, KizzasiBuilder};
use kizzasi_core::ModelType;
use scirs2_core::ndarray::Array1;
use std::collections::VecDeque;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct TuningConfig {
pub warmup_iterations: usize,
pub profiling_iterations: usize,
pub target_latency_us: Option<u64>,
pub target_throughput: Option<f64>,
pub max_memory_bytes: Option<usize>,
pub aggressive_tuning: bool,
pub conservative_tuning: bool,
}
impl Default for TuningConfig {
fn default() -> Self {
Self {
warmup_iterations: 10,
profiling_iterations: 100,
target_latency_us: None,
target_throughput: None,
max_memory_bytes: None,
aggressive_tuning: false,
conservative_tuning: false,
}
}
}
impl TuningConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_warmup(mut self, iterations: usize) -> Self {
self.warmup_iterations = iterations;
self
}
pub fn with_profiling(mut self, iterations: usize) -> Self {
self.profiling_iterations = iterations;
self
}
pub fn with_target_latency_us(mut self, latency_us: u64) -> Self {
self.target_latency_us = Some(latency_us);
self
}
pub fn with_target_throughput(mut self, throughput: f64) -> Self {
self.target_throughput = Some(throughput);
self
}
pub fn with_max_memory(mut self, bytes: usize) -> Self {
self.max_memory_bytes = Some(bytes);
self
}
pub fn aggressive(mut self) -> Self {
self.aggressive_tuning = true;
self.conservative_tuning = false;
self
}
pub fn conservative(mut self) -> Self {
self.conservative_tuning = true;
self.aggressive_tuning = false;
self
}
}
#[derive(Debug, Clone)]
pub struct WorkloadProfile {
pub avg_input_dim: usize,
pub avg_output_dim: usize,
pub avg_latency_us: u64,
pub latency_std_dev_us: f64,
pub p50_latency_us: u64,
pub p95_latency_us: u64,
pub p99_latency_us: u64,
pub throughput_pps: f64,
pub avg_error: Option<f64>,
pub estimated_memory_bytes: usize,
}
#[derive(Debug, Clone)]
pub struct TuningRecommendation {
pub model_type: ModelType,
pub hidden_dim: usize,
pub num_layers: usize,
pub context_window: usize,
pub expected_latency_us: u64,
pub expected_throughput_pps: f64,
pub expected_memory_bytes: usize,
pub confidence: f64,
pub reasoning: String,
}
pub struct AutoTuner {
config: TuningConfig,
profiling_history: VecDeque<WorkloadProfile>,
}
impl AutoTuner {
pub fn new(config: TuningConfig) -> Self {
Self {
config,
profiling_history: VecDeque::with_capacity(10),
}
}
pub fn with_defaults() -> Self {
Self::new(TuningConfig::default())
}
pub fn profile(
&mut self,
predictor: &mut Kizzasi,
input_dim: usize,
) -> KizzasiResult<WorkloadProfile> {
let mut latencies = Vec::with_capacity(self.config.profiling_iterations);
let test_input = Array1::from_vec(vec![1.0; input_dim]);
for _ in 0..self.config.warmup_iterations {
let _ = predictor.step(&test_input)?;
}
for _ in 0..self.config.profiling_iterations {
let start = Instant::now();
let _ = predictor.step(&test_input)?;
let elapsed = start.elapsed().as_micros() as u64;
latencies.push(elapsed);
}
let avg_latency_us = latencies.iter().sum::<u64>() / latencies.len() as u64;
let variance: f64 = latencies
.iter()
.map(|&lat| {
let diff = lat as f64 - avg_latency_us as f64;
diff * diff
})
.sum::<f64>()
/ latencies.len() as f64;
let latency_std_dev_us = variance.sqrt();
let mut sorted_latencies = latencies.clone();
sorted_latencies.sort_unstable();
let p50_latency_us = sorted_latencies[sorted_latencies.len() / 2];
let p95_latency_us = sorted_latencies[(sorted_latencies.len() as f64 * 0.95) as usize];
let p99_latency_us = sorted_latencies[(sorted_latencies.len() as f64 * 0.99) as usize];
let throughput_pps = 1_000_000.0 / avg_latency_us as f64;
let hidden_dim = predictor.hidden_dim();
let num_layers = predictor.num_layers();
let state_dim = predictor.state_dim();
let estimated_memory_bytes = (hidden_dim * state_dim * num_layers * 4) + (hidden_dim * hidden_dim * num_layers * 4 * 4);
let profile = WorkloadProfile {
avg_input_dim: input_dim,
avg_output_dim: predictor.output_dim(),
avg_latency_us,
latency_std_dev_us,
p50_latency_us,
p95_latency_us,
p99_latency_us,
throughput_pps,
avg_error: None,
estimated_memory_bytes,
};
self.profiling_history.push_back(profile.clone());
if self.profiling_history.len() > 10 {
self.profiling_history.pop_front();
}
Ok(profile)
}
pub fn recommend_for_latency(
&self,
input_dim: usize,
output_dim: usize,
target_latency_us: u64,
) -> TuningRecommendation {
let (model_type, hidden_dim, num_layers, context_window, reasoning) =
if target_latency_us < 100 {
(
ModelType::S4,
32,
1,
64,
"Ultra-low latency requirement - using minimal S4 model".to_string(),
)
} else if target_latency_us < 500 {
(
ModelType::Mamba2,
64,
2,
128,
"Low latency requirement - using compact Mamba2 model".to_string(),
)
} else if target_latency_us < 2000 {
(
ModelType::Mamba2,
128,
4,
256,
"Medium latency budget - using balanced Mamba2 model".to_string(),
)
} else if target_latency_us < 10000 {
(
ModelType::Rwkv,
256,
6,
512,
"High latency budget - using larger RWKV model for better accuracy".to_string(),
)
} else {
(
ModelType::Rwkv,
512,
8,
1024,
"Very high latency budget - using large model for maximum accuracy".to_string(),
)
};
let (hidden_dim, num_layers) = if self.config.aggressive_tuning {
let adjusted_layers = if num_layers > 1 { num_layers - 1 } else { 1 };
(hidden_dim / 2, adjusted_layers)
} else if self.config.conservative_tuning {
(hidden_dim * 2, num_layers + 2)
} else {
(hidden_dim, num_layers)
};
let base_latency: usize = match model_type {
ModelType::S4 => 50,
ModelType::Mamba => 100,
ModelType::Mamba2 => 80,
ModelType::Rwkv => 150,
};
let expected_latency_us =
(base_latency * (hidden_dim / 64) * num_layers * (input_dim + output_dim)
/ (input_dim.max(1))) as u64;
let expected_throughput_pps = 1_000_000.0 / expected_latency_us as f64;
let expected_memory_bytes =
(hidden_dim * 64 * num_layers * 4) + (hidden_dim * hidden_dim * num_layers * 16);
let latency_ratio = expected_latency_us as f64 / target_latency_us as f64;
let confidence = if latency_ratio <= 0.8 {
0.95 } else if latency_ratio <= 1.0 {
0.85 } else if latency_ratio <= 1.2 {
0.7 } else {
0.5 };
TuningRecommendation {
model_type,
hidden_dim,
num_layers,
context_window,
expected_latency_us,
expected_throughput_pps,
expected_memory_bytes,
confidence,
reasoning,
}
}
pub fn recommend_for_throughput(
&self,
input_dim: usize,
output_dim: usize,
target_throughput_pps: f64,
) -> TuningRecommendation {
let target_latency_us = (1_000_000.0 / target_throughput_pps) as u64;
self.recommend_for_latency(input_dim, output_dim, target_latency_us)
}
pub fn recommend_balanced(&self, input_dim: usize, output_dim: usize) -> TuningRecommendation {
let mut rec = self.recommend_for_latency(input_dim, output_dim, 1000);
rec.reasoning = "Balanced configuration for general-purpose use".to_string();
rec
}
pub fn build_for_latency(
&self,
input_dim: usize,
output_dim: usize,
target_latency_us: u64,
) -> KizzasiResult<Kizzasi> {
let rec = self.recommend_for_latency(input_dim, output_dim, target_latency_us);
KizzasiBuilder::new()
.model_type(rec.model_type)
.input_dim(input_dim)
.output_dim(output_dim)
.hidden_dim(rec.hidden_dim)
.num_layers(rec.num_layers)
.context_window(rec.context_window)
.build()
}
pub fn build_for_throughput(
&self,
input_dim: usize,
output_dim: usize,
target_throughput_pps: f64,
) -> KizzasiResult<Kizzasi> {
let rec = self.recommend_for_throughput(input_dim, output_dim, target_throughput_pps);
KizzasiBuilder::new()
.model_type(rec.model_type)
.input_dim(input_dim)
.output_dim(output_dim)
.hidden_dim(rec.hidden_dim)
.num_layers(rec.num_layers)
.context_window(rec.context_window)
.build()
}
pub fn history(&self) -> &VecDeque<WorkloadProfile> {
&self.profiling_history
}
pub fn latest_profile(&self) -> Option<&WorkloadProfile> {
self.profiling_history.back()
}
pub fn clear_history(&mut self) {
self.profiling_history.clear();
}
}
pub struct AdaptiveTuner {
tuner: AutoTuner,
current_predictor: Option<Kizzasi>,
input_dim: usize,
output_dim: usize,
adaptation_interval: usize,
predictions_since_adaptation: usize,
}
impl AdaptiveTuner {
pub fn new(
input_dim: usize,
output_dim: usize,
config: TuningConfig,
adaptation_interval: usize,
) -> Self {
Self {
tuner: AutoTuner::new(config),
current_predictor: None,
input_dim,
output_dim,
adaptation_interval,
predictions_since_adaptation: 0,
}
}
pub fn with_predictor(mut self, predictor: Kizzasi) -> Self {
self.current_predictor = Some(predictor);
self
}
pub fn predict(&mut self, input: &Array1<f32>) -> KizzasiResult<Array1<f32>> {
if self.current_predictor.is_none() {
let predictor =
KizzasiBuilder::lightweight_preset(self.input_dim, self.output_dim).build()?;
self.current_predictor = Some(predictor);
}
let predictor =
self.current_predictor
.as_mut()
.ok_or_else(|| KizzasiError::InvalidState {
reason: "Predictor not initialized".to_string(),
recovery: None,
})?;
let output = predictor.step(input)?;
self.predictions_since_adaptation += 1;
if self.predictions_since_adaptation >= self.adaptation_interval {
self.adapt()?;
self.predictions_since_adaptation = 0;
}
Ok(output)
}
fn adapt(&mut self) -> KizzasiResult<()> {
if let Some(predictor) = &mut self.current_predictor {
let profile = self.tuner.profile(predictor, self.input_dim)?;
if let Some(target_latency_us) = self.tuner.config.target_latency_us {
if profile.avg_latency_us > target_latency_us * 120 / 100 {
let rec = self.tuner.recommend_for_latency(
self.input_dim,
self.output_dim,
target_latency_us,
);
if rec.confidence > 0.7 {
let new_predictor = KizzasiBuilder::new()
.model_type(rec.model_type)
.input_dim(self.input_dim)
.output_dim(self.output_dim)
.hidden_dim(rec.hidden_dim)
.num_layers(rec.num_layers)
.context_window(rec.context_window)
.build()?;
self.current_predictor = Some(new_predictor);
}
}
}
}
Ok(())
}
pub fn predictor(&self) -> Option<&Kizzasi> {
self.current_predictor.as_ref()
}
pub fn predictor_mut(&mut self) -> Option<&mut Kizzasi> {
self.current_predictor.as_mut()
}
pub fn latest_profile(&self) -> Option<&WorkloadProfile> {
self.tuner.latest_profile()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tuning_config() {
let config = TuningConfig::default();
assert_eq!(config.warmup_iterations, 10);
assert_eq!(config.profiling_iterations, 100);
let custom = TuningConfig::new()
.with_target_latency_us(1000)
.with_warmup(20)
.aggressive();
assert_eq!(custom.target_latency_us, Some(1000));
assert_eq!(custom.warmup_iterations, 20);
assert!(custom.aggressive_tuning);
}
#[test]
fn test_autotuner_creation() {
let tuner = AutoTuner::with_defaults();
assert_eq!(tuner.profiling_history.len(), 0);
}
#[test]
fn test_latency_recommendations() {
let tuner = AutoTuner::with_defaults();
let rec1 = tuner.recommend_for_latency(2, 2, 50);
assert_eq!(rec1.model_type, ModelType::S4);
assert!(rec1.hidden_dim <= 64);
let rec2 = tuner.recommend_for_latency(64, 64, 1000);
assert_eq!(rec2.model_type, ModelType::Mamba2);
let rec3 = tuner.recommend_for_latency(128, 128, 5000);
assert_eq!(rec3.model_type, ModelType::Rwkv);
assert!(rec3.hidden_dim >= 128);
}
#[test]
fn test_throughput_recommendations() {
let tuner = AutoTuner::with_defaults();
let rec1 = tuner.recommend_for_throughput(2, 2, 100000.0); assert!(rec1.expected_latency_us < 20);
let rec2 = tuner.recommend_for_throughput(64, 64, 100.0); assert!(rec2.expected_latency_us > 1000);
}
#[test]
fn test_balanced_recommendations() {
let tuner = AutoTuner::with_defaults();
let rec = tuner.recommend_balanced(32, 32);
assert!(rec.hidden_dim >= 64);
assert!(rec.num_layers >= 2);
assert!(rec.confidence >= 0.0); assert!(rec.confidence <= 1.0);
}
#[test]
fn test_build_for_latency() -> KizzasiResult<()> {
let tuner = AutoTuner::with_defaults();
let predictor = tuner.build_for_latency(4, 4, 1000)?;
assert_eq!(predictor.input_dim(), 4);
assert_eq!(predictor.output_dim(), 4);
Ok(())
}
#[test]
fn test_profiling() -> KizzasiResult<()> {
let config = TuningConfig::new().with_warmup(2).with_profiling(10);
let mut tuner = AutoTuner::new(config);
let mut predictor = KizzasiBuilder::lightweight_preset(2, 2).build()?;
let profile = tuner.profile(&mut predictor, 2)?;
assert_eq!(profile.avg_input_dim, 2);
assert!(profile.avg_latency_us > 0);
assert!(profile.throughput_pps > 0.0);
assert_eq!(tuner.profiling_history.len(), 1);
Ok(())
}
#[test]
fn test_adaptive_tuner() -> KizzasiResult<()> {
let config = TuningConfig::new().with_target_latency_us(10000);
let mut adaptive = AdaptiveTuner::new(2, 2, config, 100);
let input = Array1::from_vec(vec![1.0, 2.0]);
let output = adaptive.predict(&input)?;
assert_eq!(output.len(), 2);
for _ in 0..10 {
let _ = adaptive.predict(&input)?;
}
assert!(adaptive.predictor().is_some());
Ok(())
}
#[test]
fn test_aggressive_tuning() {
let config = TuningConfig::default().aggressive();
let tuner = AutoTuner::new(config);
let rec1 = tuner.recommend_for_latency(64, 64, 1000);
let config_normal = TuningConfig::default();
let tuner_normal = AutoTuner::new(config_normal);
let rec2 = tuner_normal.recommend_for_latency(64, 64, 1000);
assert!(rec1.hidden_dim <= rec2.hidden_dim);
}
#[test]
fn test_conservative_tuning() {
let config = TuningConfig::default().conservative();
let tuner = AutoTuner::new(config);
let rec1 = tuner.recommend_for_latency(64, 64, 5000);
let config_normal = TuningConfig::default();
let tuner_normal = AutoTuner::new(config_normal);
let rec2 = tuner_normal.recommend_for_latency(64, 64, 5000);
assert!(rec1.hidden_dim >= rec2.hidden_dim);
}
}