#![allow( clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_markdown, clippy::empty_line_after_doc_comments, clippy::field_reassign_with_default, clippy::get_unwrap, clippy::identity_op, clippy::inconsistent_digit_grouping, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_underscore_must_use, clippy::manual_div_ceil, clippy::manual_let_else, clippy::manual_range_contains, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::non_ascii_literal, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::unseparated_literal_suffix, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, clippy::else_if_without_else, clippy::expect_used, clippy::missing_const_for_fn, clippy::similar_names, clippy::type_complexity, clippy::collapsible_else_if, clippy::doc_lazy_continuation, clippy::items_after_test_module, clippy::map_clone, clippy::multiple_unsafe_ops_per_block, clippy::unwrap_or_default, clippy::assign_op_pattern, clippy::needless_borrow, clippy::println_empty_string, clippy::unnecessary_cast, clippy::used_underscore_binding, clippy::create_dir, clippy::implicit_saturating_sub, clippy::exit, clippy::expect_fun_call, clippy::too_many_arguments, clippy::unnecessary_map_or, clippy::unwrap_used, dead_code, unused_imports, unused_variables, clippy::cloned_ref_to_slice_refs, clippy::neg_multiply, clippy::while_let_loop, clippy::bool_assert_comparison, clippy::excessive_precision, clippy::trivially_copy_pass_by_ref, clippy::op_ref, clippy::redundant_closure, clippy::unnecessary_lazy_evaluations, clippy::if_then_some_else_none, clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] //! TFT Inference Latency Benchmark for HFT Production //! //! **Objective**: Measure TFT inference latency and ensure P95 <5ms for production HFT. //! //! **Performance Targets**: //! - P95 latency: <5ms (5000us) //! - Mean latency: <2ms (2000us) //! - Consistent performance: P99/P50 ratio <2.0 //! //! **Comparison with Other Models**: //! - DQN: P95 2.1ms //! - PPO: P95 3.2ms //! - MAMBA-2: P95 1.8ms //! - TFT: P95 target <5ms //! //! **Optimization Strategies** (if >5ms): //! 1. CUDA Kernel Fusion: Reduce kernel launch overhead //! 2. Batch Size = 1: Single-sample inference (lowest latency) //! 3. Mixed Precision: FP16 inference (2x faster) //! 4. Model Quantization: INT8 inference (4x faster) //! 5. Attention Optimization: Flash Attention (2-4x faster) #![allow(unused_crate_dependencies)] use ml::tft::{TFTConfig, TemporalFusionTransformer}; use ml::MLError; use ml_supervised::gpu_tensor::GpuTensor; use std::sync::Arc; use std::time::Instant; use tracing::info; use tracing::warn; /// Helper: Create realistic TFT input tensors for benchmarking. /// Uses the TFT model's own stream for GPU allocation. fn create_tft_inputs( config: &TFTConfig, stream: &Arc, ) -> Result<(GpuTensor, GpuTensor, GpuTensor), MLError> { // Static features: [batch=1, num_static_features] let static_data = vec![0.5f32; config.num_static_features]; let static_features = GpuTensor::from_vec(static_data, &[1, config.num_static_features], stream)?; // Historical features: [batch=1, seq_len, num_unknown_features] let hist_len = config.sequence_length; let hist_dim = config.num_unknown_features; let hist_data = vec![0.5f32; hist_len * hist_dim]; let historical_features = GpuTensor::from_vec(hist_data, &[1, hist_len, hist_dim], stream)?; // Future features: [batch=1, prediction_horizon, num_known_features] let fut_len = config.prediction_horizon; let fut_dim = config.num_known_features; let fut_data = vec![0.5f32; fut_len * fut_dim]; let future_features = GpuTensor::from_vec(fut_data, &[1, fut_len, fut_dim], stream)?; Ok((static_features, historical_features, future_features)) } /// Primary benchmark: TFT inference latency with statistical analysis #[test] fn test_tft_inference_latency_p95_target() -> Result<(), MLError> { info!("=== TFT Inference Latency Benchmark ==="); info!("Target: P95 <5ms (5000us) for production HFT"); // Production-realistic TFT configuration let config = TFTConfig { input_dim: 64, hidden_dim: 128, num_heads: 8, num_layers: 3, prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 49, // 5 + 10 + 49 = 64 (fixed feature count mismatch) learning_rate: 1e-3, batch_size: 1, // HFT: Single-sample inference for lowest latency dropout_rate: 0.0, // Inference mode: No dropout l2_regularization: 1e-4, use_flash_attention: true, memory_efficient: true, max_inference_latency_us: 5000, target_throughput_pps: 100_000, }; let mut tft = TemporalFusionTransformer::new(config.clone())?; let stream = tft.stream().clone(); // Prepare inputs let (static_features, historical_features, future_features) = create_tft_inputs(&config, &stream)?; // ==================== WARMUP PHASE ==================== // Critical for CUDA: Ensure kernels are compiled and caches are warm info!("Warmup: 5 iterations (CUDA kernel compilation)"); for _ in 0..5 { let _ = tft.forward(&static_features, &historical_features, &future_features)?; } // ==================== BENCHMARK PHASE ==================== // 100 iterations for stable statistics info!("Benchmark: 100 iterations for stable statistics"); let num_iterations = 100; let mut latencies_us = Vec::with_capacity(num_iterations); for _ in 0..num_iterations { let start = Instant::now(); let _output = tft.forward(&static_features, &historical_features, &future_features)?; let elapsed_us = start.elapsed().as_micros() as u64; latencies_us.push(elapsed_us); } // ==================== STATISTICAL ANALYSIS ==================== latencies_us.sort_unstable(); let mean = latencies_us.iter().sum::() as f64 / latencies_us.len() as f64; let p50 = latencies_us[latencies_us.len() / 2]; let p95 = latencies_us[latencies_us.len() * 95 / 100]; let p99 = latencies_us[latencies_us.len() * 99 / 100]; let min = latencies_us[0]; let max = latencies_us[latencies_us.len() - 1]; // Consistency metric: P99/P50 ratio (lower is better) let consistency_ratio = p99 as f64 / p50 as f64; // ==================== RESULTS ==================== info!( mean_us = mean as u64, mean_ms = mean / 1000.0, p50_us = p50, p50_ms = p50 as f64 / 1000.0, p95_us = p95, p95_ms = p95 as f64 / 1000.0, p99_us = p99, p99_ms = p99 as f64 / 1000.0, min_us = min, max_us = max, consistency_ratio, "TFT Inference Latency Statistics" ); // ==================== VALIDATION ==================== // Primary target: P95 <5ms (5000us) if p95 < 5000 { info!(p95_us = p95, "PASS: P95 latency is <5ms target"); } else { warn!( p95_us = p95, "WARNING: P95 latency exceeds 5ms target. Optimization strategies: \ 1. Enable Flash Attention (2-4x speedup) \ 2. Mixed Precision FP16 (2x speedup) \ 3. Model Quantization INT8 (4x speedup) \ 4. Reduce hidden_dim or num_layers \ 5. CUDA kernel fusion" ); } // Secondary target: Mean <2ms if mean < 2000.0 { info!(mean_us = mean as u64, "PASS: Mean latency is <2ms"); } else { info!(mean_us = mean as u64, "INFO: Mean latency exceeds 2ms (non-critical)"); } // Consistency check: P99/P50 ratio <2.0 if consistency_ratio < 2.0 { info!(consistency_ratio, "PASS: Consistency ratio is <2.0 (stable performance)"); } else { warn!(consistency_ratio, "WARNING: Consistency ratio exceeds 2.0 (high variance)"); } // Test assertion: P95 must be <5ms for production readiness assert!( p95 < 5000, "FAIL: TFT P95 latency {}us exceeds 5ms target ({}ms)", p95, p95 as f64 / 1000.0 ); Ok(()) } /// Comparison benchmark: TFT vs other models (DQN, PPO, MAMBA-2) #[test] fn test_tft_latency_comparison_with_other_models() -> Result<(), MLError> { info!("=== Model Inference Latency Comparison ==="); // TFT benchmark (from above) let tft_config = TFTConfig { hidden_dim: 128, num_heads: 8, num_layers: 3, prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 49, // 5 + 10 + 49 = 64 (fixed feature count mismatch) batch_size: 1, dropout_rate: 0.0, ..Default::default() }; let mut tft = TemporalFusionTransformer::new(tft_config.clone())?; let stream = tft.stream().clone(); let (static_features, historical_features, future_features) = create_tft_inputs(&tft_config, &stream)?; // Warmup for _ in 0..5 { let _ = tft.forward(&static_features, &historical_features, &future_features)?; } // Benchmark TFT let mut tft_latencies = Vec::new(); for _ in 0..100 { let start = Instant::now(); let _ = tft.forward(&static_features, &historical_features, &future_features)?; tft_latencies.push(start.elapsed().as_micros() as u64); } tft_latencies.sort_unstable(); let tft_mean = tft_latencies.iter().sum::() as f64 / tft_latencies.len() as f64; let tft_p50 = tft_latencies[tft_latencies.len() / 2]; let tft_p95 = tft_latencies[tft_latencies.len() * 95 / 100]; let tft_p99 = tft_latencies[tft_latencies.len() * 99 / 100]; // ==================== COMPARISON TABLE ==================== // Reference: DQN P95=2.1ms, PPO P95=3.2ms, MAMBA-2 P95=1.8ms (all <5ms target) info!( tft_mean_us = tft_mean as u64, tft_p50_us = tft_p50, tft_p95_ms = tft_p95 as f64 / 1000.0, tft_p99_ms = tft_p99 as f64 / 1000.0, tft_meets_target = tft_p95 < 5000, "Model latency comparison: DQN=2.1ms, PPO=3.2ms, MAMBA2=1.8ms (all <5ms)" ); // Verify TFT meets target assert!( tft_p95 < 5000, "TFT P95 latency {}us exceeds 5ms target", tft_p95 ); Ok(()) } /// Test: TFT latency with different batch sizes (batch optimization) #[test] fn test_tft_batch_size_latency_tradeoff() -> Result<(), MLError> { info!("=== TFT Batch Size Latency Trade-off ==="); let batch_sizes = vec![1, 2, 4, 8]; for batch_size in batch_sizes { let config = TFTConfig { hidden_dim: 128, num_heads: 8, num_layers: 3, prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 49, // 5 + 10 + 49 = 64 (fixed feature count mismatch) batch_size, dropout_rate: 0.0, ..Default::default() }; let mut tft = TemporalFusionTransformer::new(config.clone())?; let stream = tft.stream().clone(); // Create batched inputs let static_data = vec![0.5f32; batch_size * config.num_static_features]; let static_features = GpuTensor::from_vec( static_data, &[batch_size, config.num_static_features], &stream, )?; let hist_data = vec![0.5f32; batch_size * config.sequence_length * config.num_unknown_features]; let historical_features = GpuTensor::from_vec( hist_data, &[ batch_size, config.sequence_length, config.num_unknown_features, ], &stream, )?; let fut_data = vec![0.5f32; batch_size * config.prediction_horizon * config.num_known_features]; let future_features = GpuTensor::from_vec( fut_data, &[ batch_size, config.prediction_horizon, config.num_known_features, ], &stream, )?; // Warmup for _ in 0..5 { let _ = tft.forward(&static_features, &historical_features, &future_features)?; } // Benchmark let num_iterations = 50; let mut latencies = Vec::new(); for _ in 0..num_iterations { let start = Instant::now(); let _ = tft.forward(&static_features, &historical_features, &future_features)?; latencies.push(start.elapsed().as_micros() as u64); } let avg_latency_us = latencies.iter().sum::() / num_iterations as u64; let per_sample_us = avg_latency_us as f64 / batch_size as f64; let throughput = 1_000_000.0 / per_sample_us; info!( batch_size, avg_latency_us, per_sample_us = per_sample_us as u64, throughput = throughput as u64, "Batch size latency tradeoff" ); } info!("Insight: Larger batches amortize overhead, increasing throughput. HFT recommendation: batch_size=1 for lowest latency (<5ms)"); Ok(()) } /// Test: TFT latency with Flash Attention enabled/disabled #[test] fn test_tft_flash_attention_speedup() -> Result<(), MLError> { info!("=== TFT Flash Attention Speedup ==="); // Test both configurations let flash_configs = vec![("Standard Attention", false), ("Flash Attention", true)]; let mut baseline_p95 = 0u64; for (name, use_flash) in flash_configs { let config = TFTConfig { hidden_dim: 128, num_heads: 8, num_layers: 3, prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 49, // 5 + 10 + 49 = 64 (fixed feature count mismatch) use_flash_attention: use_flash, batch_size: 1, dropout_rate: 0.0, ..Default::default() }; let mut tft = TemporalFusionTransformer::new(config.clone())?; let stream = tft.stream().clone(); let (static_features, historical_features, future_features) = create_tft_inputs(&config, &stream)?; // Warmup for _ in 0..5 { let _ = tft.forward(&static_features, &historical_features, &future_features)?; } // Benchmark let mut latencies = Vec::new(); for _ in 0..100 { let start = Instant::now(); let _ = tft.forward(&static_features, &historical_features, &future_features)?; latencies.push(start.elapsed().as_micros() as u64); } latencies.sort_unstable(); let p50 = latencies[latencies.len() / 2]; let p95 = latencies[latencies.len() * 95 / 100]; let speedup = if baseline_p95 > 0 { baseline_p95 as f64 / p95 as f64 } else { baseline_p95 = p95; 1.0 }; info!( config_name = name, p50_us = p50, p95_us = p95, speedup, "Flash attention comparison" ); } info!("Flash Attention expected speedup: 2-4x for long sequences"); Ok(()) } /// Test: TFT latency with different model sizes (hidden_dim, num_layers) #[test] fn test_tft_model_size_latency_scaling() -> Result<(), MLError> { info!("=== TFT Model Size Latency Scaling ==="); // Test configurations: (hidden_dim, num_layers, name) let model_configs = vec![ (64, 2, "Small"), (128, 3, "Medium (Production)"), (256, 4, "Large"), (512, 6, "Extra Large"), ]; for (hidden_dim, num_layers, name) in model_configs { let config = TFTConfig { hidden_dim, num_heads: 8, num_layers, prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 49, // 5 + 10 + 49 = 64 (fixed feature count mismatch) batch_size: 1, dropout_rate: 0.0, use_flash_attention: true, ..Default::default() }; let mut tft = TemporalFusionTransformer::new(config.clone())?; let stream = tft.stream().clone(); let (static_features, historical_features, future_features) = create_tft_inputs(&config, &stream)?; // Warmup for _ in 0..5 { let _ = tft.forward(&static_features, &historical_features, &future_features)?; } // Benchmark let mut latencies = Vec::new(); for _ in 0..100 { let start = Instant::now(); let _ = tft.forward(&static_features, &historical_features, &future_features)?; latencies.push(start.elapsed().as_micros() as u64); } latencies.sort_unstable(); let p95 = latencies[latencies.len() * 95 / 100]; let meets_target = p95 < 5000; info!( model_name = name, hidden_dim, num_layers, p95_us = p95, meets_target, "Model size latency scaling" ); } info!("Latency scales with model size: Smaller models = lower latency. Production: Medium (128, 3 layers) balances accuracy and latency"); Ok(()) } /// Test: TFT memory usage during inference #[test] fn test_tft_inference_memory_usage() -> Result<(), MLError> { info!("=== TFT Inference Memory Usage ==="); let config = TFTConfig { hidden_dim: 128, num_heads: 8, num_layers: 3, prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 49, // 5 + 10 + 49 = 64 (fixed feature count mismatch) batch_size: 1, dropout_rate: 0.0, ..Default::default() }; let mut tft = TemporalFusionTransformer::new(config.clone())?; let stream = tft.stream().clone(); let (static_features, historical_features, future_features) = create_tft_inputs(&config, &stream)?; // Single inference let _ = tft.forward(&static_features, &historical_features, &future_features)?; // Estimate memory usage let static_mem = config.num_static_features * 4; // f32 let hist_mem = config.sequence_length * config.num_unknown_features * 4; let fut_mem = config.prediction_horizon * config.num_known_features * 4; let output_mem = config.prediction_horizon * config.num_quantiles * 4; let total_input_mem = static_mem + hist_mem + fut_mem; let total_mem = total_input_mem + output_mem; info!( static_mem_bytes = static_mem, static_mem_kb = static_mem as f64 / 1024.0, hist_mem_bytes = hist_mem, hist_mem_kb = hist_mem as f64 / 1024.0, fut_mem_bytes = fut_mem, fut_mem_kb = fut_mem as f64 / 1024.0, output_mem_bytes = output_mem, output_mem_kb = output_mem as f64 / 1024.0, total_mem_bytes = total_mem, total_mem_kb = total_mem as f64 / 1024.0, "Memory Usage Breakdown" ); // Verify memory is reasonable assert!( total_mem < 10 * 1024 * 1024, "Memory usage exceeds 10MB target" ); Ok(()) }