mod dynamic;
#[cfg(test)]
mod tests;
pub use dynamic::{
DynamicBatchConfig, DynamicBatchConfigBuilder, DynamicBatchProcessor, DynamicBatchStats,
PaddingStrategy, PriorityLevel,
};
use crate::error::{InferenceError, MlError, Result};
use crate::models::Model;
use indicatif::{ProgressBar, ProgressStyle};
use oxigdal_core::buffer::RasterBuffer;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use sysinfo::System;
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct BatchConfig {
pub max_batch_size: usize,
pub batch_timeout_ms: u64,
pub dynamic_batching: bool,
pub parallel_batches: usize,
pub memory_pooling: bool,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
max_batch_size: 32,
batch_timeout_ms: 100,
dynamic_batching: true,
parallel_batches: 4,
memory_pooling: true,
}
}
}
impl BatchConfig {
#[must_use]
pub fn builder() -> BatchConfigBuilder {
BatchConfigBuilder::default()
}
#[must_use]
pub fn auto_tune_batch_size(sample_size_bytes: usize, memory_fraction: f32) -> usize {
let memory_fraction = memory_fraction.clamp(0.1, 0.9);
let mut system = System::new_all();
system.refresh_all();
let available_memory = system.available_memory() as usize;
let usable_memory = (available_memory as f32 * memory_fraction) as usize;
let batch_size = usable_memory
.checked_div(sample_size_bytes)
.map(|v| v.clamp(1, 256))
.unwrap_or(32);
info!(
"Auto-tuned batch size: {} (available memory: {} MB, sample size: {} MB)",
batch_size,
available_memory / (1024 * 1024),
sample_size_bytes / (1024 * 1024)
);
batch_size
}
}
#[derive(Debug, Default)]
pub struct BatchConfigBuilder {
max_batch_size: Option<usize>,
batch_timeout_ms: Option<u64>,
dynamic_batching: Option<bool>,
parallel_batches: Option<usize>,
memory_pooling: Option<bool>,
}
impl BatchConfigBuilder {
#[must_use]
pub fn max_batch_size(mut self, size: usize) -> Self {
self.max_batch_size = Some(size);
self
}
#[must_use]
pub fn batch_timeout_ms(mut self, ms: u64) -> Self {
self.batch_timeout_ms = Some(ms);
self
}
#[must_use]
pub fn dynamic_batching(mut self, enable: bool) -> Self {
self.dynamic_batching = Some(enable);
self
}
#[must_use]
pub fn parallel_batches(mut self, count: usize) -> Self {
self.parallel_batches = Some(count);
self
}
#[must_use]
pub fn memory_pooling(mut self, enable: bool) -> Self {
self.memory_pooling = Some(enable);
self
}
#[must_use]
pub fn build(self) -> BatchConfig {
BatchConfig {
max_batch_size: self.max_batch_size.unwrap_or(32),
batch_timeout_ms: self.batch_timeout_ms.unwrap_or(100),
dynamic_batching: self.dynamic_batching.unwrap_or(true),
parallel_batches: self.parallel_batches.unwrap_or(4),
memory_pooling: self.memory_pooling.unwrap_or(true),
}
}
}
pub struct BatchProcessor<M: Model> {
model: Arc<Mutex<M>>,
config: BatchConfig,
queue: Arc<Mutex<VecDeque<BatchRequest>>>,
stats: Arc<Mutex<BatchStats>>,
}
struct BatchRequest {
input: RasterBuffer,
timestamp: Instant,
}
impl BatchRequest {
fn new(input: RasterBuffer) -> Self {
Self {
input,
timestamp: Instant::now(),
}
}
fn age(&self) -> Duration {
self.timestamp.elapsed()
}
}
impl<M: Model> BatchProcessor<M> {
#[must_use]
pub fn new(model: M, config: BatchConfig) -> Self {
info!(
"Creating batch processor with max_batch_size={}, timeout={}ms",
config.max_batch_size, config.batch_timeout_ms
);
Self {
model: Arc::new(Mutex::new(model)),
config,
queue: Arc::new(Mutex::new(VecDeque::new())),
stats: Arc::new(Mutex::new(BatchStats::default())),
}
}
pub fn infer(&self, input: RasterBuffer) -> Result<RasterBuffer> {
let start_time = Instant::now();
let request = BatchRequest::new(input);
let result = if self.config.dynamic_batching {
let mut queue = self
.queue
.lock()
.map_err(|e| MlError::InvalidConfig(format!("Failed to lock queue: {}", e)))?;
queue.push_back(request);
let timeout = Duration::from_millis(self.config.batch_timeout_ms);
let should_batch = queue.len() >= self.config.max_batch_size
|| queue.front().map(|r| r.age() >= timeout).unwrap_or(false);
if should_batch {
let batch_size = queue.len().min(self.config.max_batch_size);
let batch: Vec<_> = queue.drain(..batch_size).map(|r| r.input).collect();
drop(queue);
let results = {
let mut model = self.model.lock().map_err(|e| {
MlError::InvalidConfig(format!("Failed to lock model: {}", e))
})?;
model.predict_batch(&batch)?
};
results.into_iter().next().ok_or_else(|| {
MlError::Inference(InferenceError::Failed {
reason: "No results returned from batch".to_string(),
})
})?
} else {
let our_request = queue.pop_back().ok_or_else(|| {
MlError::Inference(InferenceError::Failed {
reason: "Request disappeared from queue".to_string(),
})
})?;
drop(queue);
let mut model = self
.model
.lock()
.map_err(|e| MlError::InvalidConfig(format!("Failed to lock model: {}", e)))?;
model.predict(&our_request.input)?
}
} else {
let mut model = self
.model
.lock()
.map_err(|e| MlError::InvalidConfig(format!("Failed to lock model: {}", e)))?;
model.predict(&request.input)?
};
if let Ok(mut stats) = self.stats.lock() {
stats.total_requests += 1;
stats.total_latency_ms += start_time.elapsed().as_millis() as u64;
}
Ok(result)
}
pub fn infer_batch(&self, inputs: Vec<RasterBuffer>) -> Result<Vec<RasterBuffer>> {
self.infer_batch_with_progress(inputs, false)
}
pub fn infer_batch_with_progress(
&self,
inputs: Vec<RasterBuffer>,
show_progress: bool,
) -> Result<Vec<RasterBuffer>> {
let batch_size = inputs.len();
debug!("Processing batch of size {}", batch_size);
let start = Instant::now();
let progress = if show_progress {
let pb = ProgressBar::new(batch_size as u64);
pb.set_style(
ProgressStyle::default_bar()
.template(
"[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} ({per_sec}) {msg}",
)
.map_err(|e| crate::error::MlError::InvalidConfig(e.to_string()))?,
);
Some(pb)
} else {
None
};
let results = if self.config.parallel_batches > 1 && batch_size > 1 {
self.parallel_batch_inference_with_progress(inputs, progress.as_ref())?
} else {
let mut model = self.model.lock().map_err(|e| {
crate::error::MlError::InvalidConfig(format!("Failed to lock model: {}", e))
})?;
model.predict_batch(&inputs)?
};
if let Some(pb) = progress {
pb.finish_with_message("Batch inference complete");
}
if let Ok(mut stats) = self.stats.lock() {
stats.total_requests += batch_size;
stats.total_batches += 1;
stats.total_latency_ms += start.elapsed().as_millis() as u64;
if batch_size > stats.max_batch_size {
stats.max_batch_size = batch_size;
}
}
Ok(results)
}
fn parallel_batch_inference_with_progress(
&self,
inputs: Vec<RasterBuffer>,
progress: Option<&ProgressBar>,
) -> Result<Vec<RasterBuffer>> {
use rayon::prelude::*;
let chunk_size =
(inputs.len() + self.config.parallel_batches - 1) / self.config.parallel_batches;
debug!(
"Splitting batch into {} chunks of ~{} items",
self.config.parallel_batches, chunk_size
);
let results: Result<Vec<_>> = inputs
.par_chunks(chunk_size)
.map(|chunk| {
let chunk_results: Result<Vec<_>> = chunk
.iter()
.map(|input| {
let result = {
let mut model = self.model.lock().map_err(|e| {
crate::error::MlError::InvalidConfig(format!(
"Failed to lock model: {}",
e
))
})?;
model.predict(input)
};
if let Some(pb) = progress {
pb.inc(1);
}
result
})
.collect();
chunk_results
})
.collect();
results.map(|chunks| chunks.into_iter().flatten().collect())
}
#[must_use]
pub fn stats(&self) -> BatchStats {
self.stats.lock().map(|s| s.clone()).unwrap_or_default()
}
pub fn reset_stats(&self) {
if let Ok(mut stats) = self.stats.lock() {
*stats = BatchStats::default();
}
}
}
#[derive(Debug, Clone, Default)]
pub struct BatchStats {
pub total_requests: usize,
pub total_batches: usize,
pub max_batch_size: usize,
pub total_latency_ms: u64,
}
impl BatchStats {
#[must_use]
pub fn avg_batch_size(&self) -> f32 {
if self.total_batches > 0 {
self.total_requests as f32 / self.total_batches as f32
} else {
0.0
}
}
#[must_use]
pub fn avg_latency_ms(&self) -> f32 {
if self.total_requests > 0 {
self.total_latency_ms as f32 / self.total_requests as f32
} else {
0.0
}
}
#[must_use]
pub fn throughput(&self) -> f32 {
if self.total_latency_ms > 0 {
(self.total_requests as f32 * 1000.0) / self.total_latency_ms as f32
} else {
0.0
}
}
}
pub struct BatchScheduler {
config: BatchConfig,
pending: VecDeque<BatchRequest>,
last_batch: Instant,
}
impl BatchScheduler {
#[must_use]
pub fn new(config: BatchConfig) -> Self {
Self {
config,
pending: VecDeque::new(),
last_batch: Instant::now(),
}
}
pub fn add_request(&mut self, input: RasterBuffer) {
self.pending.push_back(BatchRequest::new(input));
}
#[must_use]
pub fn should_form_batch(&self) -> bool {
if self.pending.len() >= self.config.max_batch_size {
return true;
}
if !self.pending.is_empty() {
let timeout = Duration::from_millis(self.config.batch_timeout_ms);
if self.last_batch.elapsed() >= timeout {
return true;
}
}
false
}
#[must_use]
pub fn form_batch(&mut self) -> Vec<RasterBuffer> {
let batch_size = self.pending.len().min(self.config.max_batch_size);
let batch: Vec<_> = self
.pending
.drain(..batch_size)
.map(|req| {
let age = req.age();
if age.as_millis() > 500 {
warn!("Request aged {}ms before batching", age.as_millis());
}
req.input
})
.collect();
self.last_batch = Instant::now();
batch
}
#[must_use]
pub fn pending_count(&self) -> usize {
self.pending.len()
}
}