use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex};
use crate::ai_inference::{
engine_interface::{InferenceResult, ModelHandle},
tensor_ops::Tensor,
};
use anyhow::Result;
use std::sync::RwLock;
#[derive(Debug, Clone, PartialEq)]
pub enum BatchStrategy {
Fixed(usize),
Dynamic(DynamicConfig),
Adaptive(AdaptiveConfig),
}
#[derive(Debug, Clone, PartialEq)]
pub struct DynamicConfig {
pub min_batch_size: usize,
pub max_batch_size: usize,
pub target_latency_ms: f64,
pub batch_timeout_ms: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AdaptiveConfig {
pub initial_batch_size: usize,
pub performance_window: usize,
pub latency_threshold_ms: f64,
pub throughput_target: f64,
}
#[derive(Debug)]
struct BatchItem {
input: Tensor,
sender: tokio::sync::oneshot::Sender<Result<InferenceResult>>,
received_at: Instant,
}
#[derive(Debug)]
pub struct BatchProcessor {
strategy: BatchStrategy,
current_batch: Arc<RwLock<Vec<BatchItem>>>,
stats: Arc<Mutex<BatchStats>>,
processing_task: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
}
#[derive(Debug, Clone)]
pub struct BatchStats {
pub total_batches: u64,
pub total_items: u64,
pub average_batch_size: f64,
pub average_latency_ms: f64,
pub throughput: f64,
pub gpu_utilization: f64,
}
impl BatchProcessor {
pub fn new(strategy: BatchStrategy) -> Self {
BatchProcessor {
strategy,
current_batch: Arc::new(Mutex::new(Vec::new())),
stats: Arc::new(Mutex::new(BatchStats {
total_batches: 0,
total_items: 0,
average_batch_size: 0.0,
average_latency_ms: 0.0,
throughput: 0.0,
gpu_utilization: 0.0,
})),
processing_task: Arc::new(Mutex::new(None)),
}
}
pub async fn add_request(&self, model: &ModelHandle, input: Tensor) -> Result<InferenceResult> {
let (sender, receiver) = tokio::sync::oneshot::channel();
let item: _ = BatchItem {
input,
sender,
received_at: Instant::now(),
};
{
let mut batch = self.current_batch.write().await;
batch.push(item);
}
self.check_and_process_batch().await?;
Ok(receiver.await??)
}
async fn check_and_process_batch(&self) -> Result<()> {
let batch_size: _ = {
let batch: _ = self.current_batch.read().await;
batch.len()
};
let should_process: _ = match &self.strategy {
BatchStrategy::Fixed(size) => batch_size >= *size,
BatchStrategy::Dynamic(config) => {
batch_size >= config.min_batch_size
|| self.should_timeout(config.batch_timeout_ms).await
}
BatchStrategy::Adaptive(config) => {
batch_size >= config.initial_batch_size || self.should_timeout(100).await
}
};
if should_process {
self.process_batch().await?;
}
Ok(())
}
async fn should_timeout(&self, timeout_ms: u64) -> bool {
let batch: _ = self.current_batch.read().await;
if let Some(first_item) = batch.first() {
let elapsed: _ = first_item.received_at.elapsed();
elapsed >= Duration::from_millis(timeout_ms)
} else {
false
}
}
async fn process_batch(&self) -> Result<()> {
let items: _ = {
let mut batch = self.current_batch.write().await;
let items: _ = batch.split_off(0);
items
};
if items.is_empty() {
return Ok(());
}
let items_count: _ = items.len();
let _current_batch: _ = Arc::clone(&self.current_batch);
let stats: _ = Arc::clone(&self.stats);
let handle: _ = tokio::spawn(async move {
let start: _ = Instant::now();
let outputs: _ = simulate_batch_inference(&items).await;
let processing_time: _ = start.elapsed();
for (item, output) in items.into_iter().zip(outputs) {
let latency: _ = item.received_at.elapsed();
let result: _ = InferenceResult {
output,
inference_time_ms: latency.as_secs_f64() * 1000.0,
model_id: "batch_model".to_string(),
gpu_used: true,
};
let _: _ = item.sender.send(Ok(result));
}
{
let mut stats = stats.lock().unwrap();
stats.total_batches += 1;
stats.total_items += items_count as u64;
stats.average_batch_size = stats.total_items as f64 / stats.total_batches as f64;
stats.average_latency_ms =
(stats.average_latency_ms + processing_time.as_secs_f64() * 1000.0) / 2.0;
stats.throughput = stats.total_items as f64
/ (stats.total_batches as f64 * processing_time.as_secs_f64());
}
});
{
let mut task = self.processing_task.lock().unwrap();
*task = Some(handle);
}
Ok(())
}
pub fn get_stats(&self) -> BatchStats {
self.stats.lock().unwrap().clone()
}
pub async fn flush(&self) -> Result<()> {
let has_items: _ = {
let batch: _ = self.current_batch.read().await;
!batch.is_empty()
};
if has_items {
self.process_batch().await?;
}
Ok(())
}
}
async fn simulate_batch_inference(items: &[BatchItem]) -> Vec<Tensor> {
let batch_size: _ = items.len();
let processing_time: _ = match batch_size {
1 => Duration::from_millis(15),
2..=4 => Duration::from_millis(20),
5..=8 => Duration::from_millis(30),
_ => Duration::from_millis(50),
};
tokio::time::sleep(processing_time).await;
let mut outputs = Vec::new();
for _ in 0..batch_size {
let output: _ = Tensor::new(vec![1.0; 1000], vec![1, 1000]).unwrap();
outputs.push(output);
}
outputs
}
#[derive(Debug)]
pub struct SmartBatchProcessor {
processor: BatchProcessor,
performance_monitor: PerformanceMonitor,
adaptive_params: Arc<Mutex<AdaptiveParams>>,
}
#[derive(Debug)]
struct PerformanceMonitor {
latency_history: Arc<Mutex<Vec<f64>>>,
throughput_history: Arc<Mutex<Vec<f64>>>,
window_size: usize,
}
#[derive(Debug, Clone)]
struct AdaptiveParams {
current_batch_size: usize,
min_batch_size: usize,
max_batch_size: usize,
latency_target: f64,
throughput_target: f64,
adjustment_factor: f64,
}
impl SmartBatchProcessor {
pub fn new(initial_batch_size: usize) -> Self {
let strategy: _ = BatchStrategy::Adaptive(AdaptiveConfig {
initial_batch_size,
performance_window: 100,
latency_threshold_ms: 10.0,
throughput_target: 1000.0,
});
SmartBatchProcessor {
processor: BatchProcessor::new(strategy),
performance_monitor: PerformanceMonitor::new(100),
adaptive_params: Arc::new(Mutex::new(AdaptiveParams {
current_batch_size: initial_batch_size,
min_batch_size: 1,
max_batch_size: 64,
latency_target: 10.0,
throughput_target: 1000.0,
adjustment_factor: 0.1,
})),
}
}
pub async fn add_request(&self, model: &ModelHandle, input: Tensor) -> Result<InferenceResult> {
let result: _ = self.processor.add_request(model, input).await?;
self.performance_monitor
.record_latency(result.inference_time_ms);
self.adapt_batch_size().await?;
Ok(result)
}
async fn adapt_batch_size(&self) -> Result<()> {
let avg_latency: _ = self.performance_monitor.get_average_latency();
let current_throughput: _ = self.performance_monitor.get_average_throughput();
let mut params = self.adaptive_params.lock().unwrap();
if avg_latency > params.latency_target {
if params.current_batch_size > params.min_batch_size {
let new_size: _ =
(params.current_batch_size as f64 * (1.0 - params.adjustment_factor)) as usize;
params.current_batch_size = new_size.max(params.min_batch_size);
}
} else if current_throughput < params.throughput_target {
if params.current_batch_size < params.max_batch_size {
let new_size: _ =
(params.current_batch_size as f64 * (1.0 + params.adjustment_factor)) as usize;
params.current_batch_size = new_size.min(params.max_batch_size);
}
}
Ok(())
}
pub async fn flush(&self) -> Result<()> {
self.processor.flush().await
}
pub fn get_performance_stats(&self) -> PerformanceStats {
let params: _ = self.adaptive_params.lock().unwrap();
PerformanceStats {
current_batch_size: params.current_batch_size,
average_latency: self.performance_monitor.get_average_latency(),
average_throughput: self.performance_monitor.get_average_throughput(),
batch_stats: self.processor.get_stats(),
}
}
}
#[derive(Debug, Clone)]
pub struct PerformanceStats {
pub current_batch_size: usize,
pub average_latency: f64,
pub average_throughput: f64,
pub batch_stats: BatchStats,
}
impl PerformanceMonitor {
fn new(window_size: usize) -> Self {
PerformanceMonitor {
latency_history: Arc::new(Mutex::new(Vec::with_capacity(window_size))),
throughput_history: Arc::new(Mutex::new(Vec::with_capacity(window_size))),
window_size,
}
}
fn record_latency(&self, latency_ms: f64) {
let mut history = self.latency_history.lock().unwrap();
if history.len() >= self.window_size {
history.remove(0);
}
history.push(latency_ms);
}
fn record_throughput(&self, throughput: f64) {
let mut history = self.throughput_history.lock().unwrap();
if history.len() >= self.window_size {
history.remove(0);
}
history.push(throughput);
}
fn get_average_latency(&self) -> f64 {
let history: _ = self.latency_history.lock().unwrap();
if history.is_empty() {
0.0
} else {
history.iter().sum::<f64>() / history.len() as f64
}
}
fn get_average_throughput(&self) -> f64 {
let history: _ = self.throughput_history.lock().unwrap();
if history.is_empty() {
0.0
} else {
history.iter().sum::<f64>() / history.len() as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[tokio::test]
async fn test_batch_processor_fixed() -> Result<()> {
let processor: _ = BatchProcessor::new(BatchStrategy::Fixed(4));
let model: _ = ModelHandle {
id: "test".to_string(),
path: "test.onnx".to_string(),
format: crate::ai_inference::engine_interface::ModelFormat::ONNX,
input_shape: vec![1, 3, 224, 224],
output_shape: vec![1, 1000],
metadata: std::collections::HashMap::new(),
};
for _ in 0..4 {
let input: _ = Tensor::new(vec![1.0; 3 * 224 * 224], vec![1, 3, 224, 224])?;
let _: _ = processor.add_request(&model, input).await?;
}
let stats: _ = processor.get_stats();
assert_eq!(stats.total_batches, 1);
assert_eq!(stats.total_items, 4);
Ok(())
}
#[tokio::test]
async fn test_batch_processor_dynamic() -> Result<()> {
let config: _ = DynamicConfig {
min_batch_size: 2,
max_batch_size: 8,
target_latency_ms: 10.0,
batch_timeout_ms: 100,
};
let processor: _ = BatchProcessor::new(BatchStrategy::Dynamic(config));
let model: _ = ModelHandle {
id: "test".to_string(),
path: "test.onnx".to_string(),
format: crate::ai_inference::engine_interface::ModelFormat::ONNX,
input_shape: vec![1, 3, 224, 224],
output_shape: vec![1, 1000],
metadata: std::collections::HashMap::new(),
};
for _ in 0..2 {
let input: _ = Tensor::new(vec![1.0; 3 * 224 * 224], vec![1, 3, 224, 224])?;
let _: _ = processor.add_request(&model, input).await?;
}
let stats: _ = processor.get_stats();
assert_eq!(stats.total_batches, 1);
assert_eq!(stats.total_items, 2);
Ok(())
}
#[tokio::test]
async fn test_smart_batch_processor() -> Result<()> {
let processor: _ = SmartBatchProcessor::new(4);
let model: _ = ModelHandle {
id: "test".to_string(),
path: "test.onnx".to_string(),
format: crate::ai_inference::engine_interface::ModelFormat::ONNX,
input_shape: vec![1, 3, 224, 224],
output_shape: vec![1, 1000],
metadata: std::collections::HashMap::new(),
};
for _ in 0..10 {
let input: _ = Tensor::new(vec![1.0; 3 * 224 * 224], vec![1, 3, 224, 224])?;
let _: _ = processor.add_request(&model, input).await?;
}
let perf_stats: _ = processor.get_performance_stats();
assert!(perf_stats.current_batch_size >= 1);
assert!(perf_stats.current_batch_size <= 64);
Ok(())
}
}