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>
304 lines
8.8 KiB
Rust
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
|
|
);
|
|
}
|
|
}
|