Files
foxhunt/ml/benches/inference_bench.rs
jgrusewski 6093eac7bf 🔧 Tonic 0.14 Upgrade: Auto-generated and build system changes
Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade

Files updated:
- Cargo.lock: Dependency resolution for Tonic 0.14.2
- All build.rs: Updated for tonic-prost-build
- Proto files: Regenerated with tonic-prost 0.14
- Examples/tests: Updated for new gRPC API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 07:34:26 +02:00

304 lines
8.8 KiB
Rust

//! ML Inference Performance Benchmarks
//!
//! Validates ML model inference latency targets for HFT trading system.
//!
//! Performance Targets:
//! - Ensemble inference: <300ms
//! - Single model inference: <100ms
//! - Feature preparation: <10ms
//! - Model switching: <50ms
#![allow(unused_crate_dependencies)]
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use std::time::Duration;
/// Simulated market features for inference
#[derive(Clone)]
struct MarketFeatures {
prices: Vec<f64>,
volumes: Vec<f64>,
#[allow(dead_code)]
order_book_depth: Vec<(f64, f64)>, // (bid, ask) pairs
#[allow(dead_code)]
timestamp_features: Vec<f64>,
}
impl MarketFeatures {
fn new() -> Self {
Self {
prices: vec![150.0; 60], // 60 time steps
volumes: vec![1000.0; 60],
order_book_depth: vec![(149.9, 150.1); 10],
timestamp_features: vec![0.0, 0.1, 0.2, 0.3, 0.4],
}
}
}
/// Mock ensemble inference
struct MockEnsembleInference {
model_count: usize,
}
impl MockEnsembleInference {
fn new(model_count: usize) -> Self {
Self { model_count }
}
fn predict(&self, features: &MarketFeatures) -> f64 {
// Simulate ensemble inference with some computation
let mut result = 0.0;
for _ in 0..self.model_count {
// Simulate model inference
for &price in &features.prices {
result += (price * 0.001).tanh();
}
for &vol in &features.volumes {
result += (vol * 0.0001).ln();
}
}
result / self.model_count as f64
}
}
/// Benchmark ensemble inference
fn bench_ensemble_inference(c: &mut Criterion) {
let ensemble = MockEnsembleInference::new(5); // 5 models in ensemble
let features = MarketFeatures::new();
c.bench_function("ensemble_inference_5_models", |b| {
b.iter_batched(
|| features.clone(),
|features| black_box(ensemble.predict(&features)),
BatchSize::SmallInput,
)
});
// Test different ensemble sizes
let mut group = c.benchmark_group("ensemble_size_comparison");
for size in [1, 3, 5, 7, 10] {
let ensemble = MockEnsembleInference::new(size);
group.bench_function(format!("{}_models", size), |b| {
b.iter_batched(
|| features.clone(),
|features| black_box(ensemble.predict(&features)),
BatchSize::SmallInput,
)
});
}
group.finish();
}
/// Benchmark feature preparation
fn bench_feature_preparation(c: &mut Criterion) {
let prices = vec![150.0 + (0..100).map(|i| i as f64 * 0.1).sum::<f64>(); 100];
let volumes = vec![1000.0; 100];
c.bench_function("feature_preparation", |b| {
b.iter(|| {
let features = MarketFeatures {
prices: black_box(&prices).to_vec(),
volumes: black_box(&volumes).to_vec(),
order_book_depth: vec![(149.9, 150.1); 10],
timestamp_features: vec![0.0; 5],
};
black_box(features)
})
});
// Test feature normalization
c.bench_function("feature_normalization", |b| {
b.iter(|| {
let normalized: Vec<f64> = prices
.iter()
.map(|&p| {
let mean = 150.0;
let std = 10.0;
(p - mean) / std
})
.collect();
black_box(normalized)
})
});
}
/// Benchmark single model inference
fn bench_single_model_inference(c: &mut Criterion) {
let model = MockEnsembleInference::new(1);
let features = MarketFeatures::new();
c.bench_function("single_model_inference", |b| {
b.iter_batched(
|| features.clone(),
|features| black_box(model.predict(&features)),
BatchSize::SmallInput,
)
});
}
/// Benchmark batch inference
fn bench_batch_inference(c: &mut Criterion) {
let model = MockEnsembleInference::new(5);
let batch: Vec<MarketFeatures> = (0..10).map(|_| MarketFeatures::new()).collect();
c.bench_function("batch_inference_10_samples", |b| {
b.iter_batched(
|| batch.clone(),
|batch| {
batch
.iter()
.map(|features| model.predict(features))
.collect::<Vec<_>>()
},
BatchSize::SmallInput,
)
});
// Test different batch sizes
let mut group = c.benchmark_group("batch_size_comparison");
for batch_size in [1, 5, 10, 20, 50] {
let batch: Vec<MarketFeatures> = (0..batch_size).map(|_| MarketFeatures::new()).collect();
group.bench_function(format!("batch_{}", batch_size), |b| {
b.iter_batched(
|| batch.clone(),
|batch| {
batch
.iter()
.map(|features| model.predict(features))
.collect::<Vec<_>>()
},
BatchSize::SmallInput,
)
});
}
group.finish();
}
criterion_group! {
name = ml_inference_benchmarks;
config = Criterion::default()
.measurement_time(Duration::from_secs(10))
.sample_size(100)
.warm_up_time(Duration::from_secs(3));
targets =
bench_ensemble_inference,
bench_single_model_inference,
bench_feature_preparation,
bench_batch_inference
}
criterion_main!(ml_inference_benchmarks);
#[cfg(test)]
mod performance_tests {
use super::*;
use std::time::Instant;
#[test]
fn test_ensemble_inference_latency() {
let ensemble = MockEnsembleInference::new(5);
let features = MarketFeatures::new();
let iterations = 100;
let start = Instant::now();
for _ in 0..iterations {
black_box(ensemble.predict(&features));
}
let duration = start.elapsed();
let avg_duration_ms = duration.as_millis() / iterations;
// Target: <300ms for ensemble inference
assert!(
avg_duration_ms < 300,
"Ensemble inference too slow: {}ms average (target: <300ms)",
avg_duration_ms
);
println!("✓ Ensemble inference: {}ms average", avg_duration_ms);
}
#[test]
fn test_single_model_latency() {
let model = MockEnsembleInference::new(1);
let features = MarketFeatures::new();
let iterations = 100;
let start = Instant::now();
for _ in 0..iterations {
black_box(model.predict(&features));
}
let duration = start.elapsed();
let avg_duration_ms = duration.as_millis() / iterations;
// Target: <100ms for single model
assert!(
avg_duration_ms < 100,
"Single model inference too slow: {}ms average (target: <100ms)",
avg_duration_ms
);
println!("✓ Single model inference: {}ms average", avg_duration_ms);
}
#[test]
fn test_feature_preparation_latency() {
let prices = vec![150.0; 100];
let volumes = vec![1000.0; 100];
let iterations = 1000;
let start = Instant::now();
for _ in 0..iterations {
let features = MarketFeatures {
prices: prices.clone(),
volumes: volumes.clone(),
order_book_depth: vec![(149.9, 150.1); 10],
timestamp_features: vec![0.0; 5],
};
black_box(features);
}
let duration = start.elapsed();
let avg_duration_us = duration.as_micros() / iterations;
// Target: <10ms (10000μs) for feature preparation
assert!(
avg_duration_us < 10000,
"Feature preparation too slow: {}μs average (target: <10000μs)",
avg_duration_us
);
println!("✓ Feature preparation: {}μs average", avg_duration_us);
}
#[test]
fn test_batch_inference_throughput() {
let model = MockEnsembleInference::new(5);
let batch: Vec<MarketFeatures> = (0..10).map(|_| MarketFeatures::new()).collect();
let iterations = 10;
let start = Instant::now();
for _ in 0..iterations {
let results: Vec<f64> = batch.iter().map(|f| model.predict(f)).collect();
black_box(results);
}
let duration = start.elapsed();
let total_predictions = iterations * 10;
let avg_per_prediction_ms = duration.as_millis() / total_predictions;
println!(
"✓ Batch inference throughput: {}ms per prediction (batch size: 10)",
avg_per_prediction_ms
);
println!(
" Total: {} predictions in {:?}",
total_predictions, duration
);
}
}