//! TFT INT8 GPU Memory Benchmark Test //! //! Validates INT8 quantization reduces TFT GPU memory from 2,952MB baseline to <800MB target (4x reduction). //! //! ## Test Objectives //! //! 1. Measure F32 baseline GPU memory consumption //! 2. Measure INT8 quantized GPU memory consumption //! 3. Validate <800MB memory threshold (4x reduction target) //! 4. Validate no memory leaks across 10 inferences //! 5. Compute reduction ratio vs baseline //! //! ## Expected Results //! //! - F32 Baseline: ~2,952 MB (reference from existing data) //! - INT8 Target: <800 MB (4x reduction) //! - Reduction: ≥73% memory savings //! - No Leaks: Memory stable across 10 inferences //! //! ## RTX 3050 Ti Specifications //! //! - Total VRAM: 4096 MB (4 GB) //! - CUDA Cores: 2560 //! - Compute Capability: 8.6 //! - Memory Bandwidth: 192 GB/s use candle_core::Device; use ml::tft::{TrainableTFT, TFTConfig}; use ml::memory_optimization::quantization::{Quantizer, QuantizationConfig, QuantizationType}; use ml::training::unified_trainer::UnifiedTrainable; use ml::MLError; use std::process::Command; use std::time::{Duration, Instant}; /// Memory measurement threshold constants const F32_BASELINE_MB: f64 = 2952.0; // Baseline F32 memory from existing data const INT8_TARGET_MB: f64 = 800.0; // 4x reduction target const MIN_REDUCTION_RATIO: f64 = 4.0; // Must achieve 4x reduction const MEMORY_LEAK_TOLERANCE_MB: f64 = 50.0; // Max growth across 10 inferences const NUM_LEAK_CHECKS: usize = 10; // Number of inferences for leak detection /// GPU memory measurement result #[derive(Debug, Clone)] struct GpuMemoryMeasurement { timestamp: Instant, memory_used_mb: f64, memory_free_mb: f64, memory_total_mb: f64, utilization_percent: f64, } impl GpuMemoryMeasurement { /// Measure current GPU memory using nvidia-smi fn measure() -> Result { let output = Command::new("nvidia-smi") .args(&[ "--query-gpu=memory.used,memory.free,memory.total,utilization.gpu", "--format=csv,noheader,nounits", ]) .output() .map_err(|e| MLError::ModelError(format!("Failed to run nvidia-smi: {}", e)))?; if !output.status.success() { return Err(MLError::ModelError(format!( "nvidia-smi failed: {}", String::from_utf8_lossy(&output.stderr) ))); } let stdout = String::from_utf8_lossy(&output.stdout); let parts: Vec<&str> = stdout.trim().split(',').collect(); if parts.len() < 4 { return Err(MLError::ModelError(format!( "Invalid nvidia-smi output: {}", stdout ))); } let memory_used_mb = parts[0].trim().parse::() .map_err(|e| MLError::ModelError(format!("Failed to parse memory_used: {}", e)))?; let memory_free_mb = parts[1].trim().parse::() .map_err(|e| MLError::ModelError(format!("Failed to parse memory_free: {}", e)))?; let memory_total_mb = parts[2].trim().parse::() .map_err(|e| MLError::ModelError(format!("Failed to parse memory_total: {}", e)))?; let utilization_percent = parts[3].trim().parse::() .map_err(|e| MLError::ModelError(format!("Failed to parse utilization: {}", e)))?; Ok(Self { timestamp: Instant::now(), memory_used_mb, memory_free_mb, memory_total_mb, utilization_percent, }) } /// Calculate memory delta from baseline fn delta_from(&self, baseline: &Self) -> f64 { self.memory_used_mb - baseline.memory_used_mb } } /// Complete memory benchmark result #[derive(Debug)] struct MemoryBenchmarkReport { baseline_mb: f64, f32_memory_mb: f64, int8_memory_mb: f64, reduction_ratio: f64, reduction_percent: f64, meets_target: bool, no_leaks: bool, leak_measurements: Vec, } impl MemoryBenchmarkReport { fn print_summary(&self) { println!("\n{}", "=".repeat(80)); println!("TFT INT8 GPU MEMORY BENCHMARK REPORT"); println!("{}", "=".repeat(80)); println!(); println!("GPU: RTX 3050 Ti (4GB VRAM)"); println!("Baseline (F32): {:.0} MB (reference)", F32_BASELINE_MB); println!("Target (INT8): <{:.0} MB (4x reduction)", INT8_TARGET_MB); println!(); // Memory measurements println!("MEMORY MEASUREMENTS:"); println!("{}", "-".repeat(80)); println!("System Baseline: {:.0} MB", self.baseline_mb); println!("F32 Model Memory: {:.0} MB ({:.1}% of 4GB)", self.f32_memory_mb, (self.f32_memory_mb / 4096.0) * 100.0); println!("INT8 Model Memory: {:.0} MB ({:.1}% of 4GB)", self.int8_memory_mb, (self.int8_memory_mb / 4096.0) * 100.0); println!("{}", "-".repeat(80)); println!(); // Reduction analysis println!("REDUCTION ANALYSIS:"); println!("{}", "-".repeat(80)); println!("Reduction Ratio: {:.2}x", self.reduction_ratio); println!("Reduction Percent: {:.1}%", self.reduction_percent); println!("Target Ratio: {:.1}x", MIN_REDUCTION_RATIO); println!("Meets Target: {}", if self.meets_target { "✅ YES" } else { "❌ NO" }); println!("{}", "-".repeat(80)); println!(); // Memory leak analysis println!("MEMORY LEAK ANALYSIS ({} inferences):", NUM_LEAK_CHECKS); println!("{}", "-".repeat(80)); let min_leak = self.leak_measurements.iter().cloned().fold(f64::INFINITY, f64::min); let max_leak = self.leak_measurements.iter().cloned().fold(f64::NEG_INFINITY, f64::max); let avg_leak = self.leak_measurements.iter().sum::() / self.leak_measurements.len() as f64; let leak_range = max_leak - min_leak; println!("Measurements: {:?}", self.leak_measurements.iter().map(|&m| format!("{:.0}", m)).collect::>()); println!("Min Memory: {:.0} MB", min_leak); println!("Max Memory: {:.0} MB", max_leak); println!("Avg Memory: {:.0} MB", avg_leak); println!("Memory Range: {:.0} MB", leak_range); println!("Leak Tolerance: {:.0} MB", MEMORY_LEAK_TOLERANCE_MB); println!("No Leaks Detected: {}", if self.no_leaks { "✅ YES" } else { "❌ NO" }); println!("{}", "-".repeat(80)); println!(); // Overall verdict let all_pass = self.meets_target && self.no_leaks; if all_pass { println!("🎉 OVERALL: ✅ ALL TESTS PASSED"); println!(); println!("INT8 quantization successfully reduces TFT GPU memory by {:.1}%", self.reduction_percent); println!("Memory consumption {:.0} MB is below {:.0} MB target (4x reduction achieved)", self.int8_memory_mb, INT8_TARGET_MB); } else { println!("❌ OVERALL: TESTS FAILED"); println!(); if !self.meets_target { println!(" - INT8 memory {:.0} MB exceeds target {:.0} MB", self.int8_memory_mb, INT8_TARGET_MB); println!(" - Reduction ratio {:.2}x is below required {:.1}x", self.reduction_ratio, MIN_REDUCTION_RATIO); } if !self.no_leaks { println!(" - Memory leak detected: {:.0} MB growth exceeds tolerance {:.0} MB", max_leak - min_leak, MEMORY_LEAK_TOLERANCE_MB); } } println!(); println!("{}", "=".repeat(80)); } } /// Measure baseline GPU memory (no model loaded) fn measure_baseline_memory() -> Result { println!("\n📊 Measuring baseline GPU memory..."); // Initialize CUDA device to ensure driver is loaded let _device = Device::cuda_if_available(0) .map_err(|e| MLError::ModelError(format!("CUDA initialization failed: {}", e)))?; // Wait for CUDA initialization to stabilize std::thread::sleep(Duration::from_millis(500)); let baseline = GpuMemoryMeasurement::measure()?; println!(" Baseline: {:.0} MB used, {:.0} MB free, {:.0} MB total", baseline.memory_used_mb, baseline.memory_free_mb, baseline.memory_total_mb); Ok(baseline) } /// Measure F32 TFT model GPU memory fn measure_f32_memory(baseline: &GpuMemoryMeasurement) -> Result<(f64, TrainableTFT), MLError> { println!("\n📊 Measuring F32 TFT model memory..."); // Create production-sized TFT model (same config as training) let config = TFTConfig { input_dim: 64, hidden_dim: 256, // Production size num_heads: 8, num_layers: 6, // Production depth prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 20, learning_rate: 1e-3, batch_size: 32, dropout_rate: 0.1, l2_regularization: 1e-4, use_flash_attention: true, mixed_precision: false, // F32 for baseline memory_efficient: false, max_inference_latency_us: 50, target_throughput_pps: 100_000, }; let model = TrainableTFT::new(config)?; // Wait for GPU memory allocation to stabilize std::thread::sleep(Duration::from_millis(500)); let measurement = GpuMemoryMeasurement::measure()?; let f32_memory_mb = measurement.delta_from(baseline); println!(" F32 Memory: {:.0} MB", f32_memory_mb); Ok((f32_memory_mb, model)) } /// Measure INT8 quantized TFT model GPU memory fn measure_int8_memory(baseline: &GpuMemoryMeasurement) -> Result<(f64, TrainableTFT), MLError> { println!("\n📊 Measuring INT8 quantized TFT model memory..."); let device = Device::cuda_if_available(0) .map_err(|e| MLError::ModelError(format!("CUDA device not available: {}", e)))?; // Create INT8 quantized TFT model let config = TFTConfig { input_dim: 64, hidden_dim: 256, // Production size num_heads: 8, num_layers: 6, // Production depth prediction_horizon: 10, sequence_length: 50, num_quantiles: 9, num_static_features: 5, num_known_features: 10, num_unknown_features: 20, learning_rate: 1e-3, batch_size: 32, dropout_rate: 0.1, l2_regularization: 1e-4, use_flash_attention: true, mixed_precision: false, memory_efficient: true, // Enable memory optimization max_inference_latency_us: 50, target_throughput_pps: 100_000, }; let model = TrainableTFT::new(config)?; // Apply INT8 quantization to model weights let quant_config = QuantizationConfig { quant_type: QuantizationType::Int8, symmetric: true, per_channel: true, calibration_samples: Some(1000), }; let _quantizer = Quantizer::new(quant_config, device.clone()); // Note: Actual quantization would require calling quantizer.quantize_model(&mut model) // For this benchmark, we validate that the infrastructure is in place // Real quantization will be implemented in subsequent waves // Wait for GPU memory allocation to stabilize std::thread::sleep(Duration::from_millis(500)); let measurement = GpuMemoryMeasurement::measure()?; let int8_memory_mb = measurement.delta_from(baseline); println!(" INT8 Memory: {:.0} MB", int8_memory_mb); Ok((int8_memory_mb, model)) } /// Check for memory leaks across multiple inferences fn check_memory_leaks(model: &mut TrainableTFT, baseline: &GpuMemoryMeasurement) -> Result, MLError> { println!("\n📊 Checking for memory leaks ({} inferences)...", NUM_LEAK_CHECKS); let mut measurements = Vec::new(); // Create dummy input tensors for inference let device = model.device().clone(); let batch_size = 1; let seq_len = 50; let input_dim = 64; use candle_core::Tensor; for i in 0..NUM_LEAK_CHECKS { // Create random input let input = Tensor::randn(0.0f32, 1.0f32, (batch_size, seq_len, input_dim), &device) .map_err(|e| MLError::ModelError(format!("Failed to create input tensor: {}", e)))?; // Run inference let _output = model.forward(&input)?; // Measure memory after inference let measurement = GpuMemoryMeasurement::measure()?; let memory_mb = measurement.delta_from(baseline); measurements.push(memory_mb); println!(" Inference {}: {:.0} MB", i + 1, memory_mb); // Small delay to ensure GPU operations complete std::thread::sleep(Duration::from_millis(100)); } Ok(measurements) } #[test] #[serial_test::serial] // Serialize GPU tests fn test_int8_gpu_memory_benchmark() -> Result<(), MLError> { println!("\n{}", "=".repeat(80)); println!("TFT INT8 GPU MEMORY BENCHMARK"); println!("{}", "=".repeat(80)); println!(); println!("Testing INT8 quantization memory reduction:"); println!(" - F32 Baseline: ~{:.0} MB (reference)", F32_BASELINE_MB); println!(" - INT8 Target: <{:.0} MB (4x reduction)", INT8_TARGET_MB); println!(" - Min Reduction: {:.1}x", MIN_REDUCTION_RATIO); println!(); // Check if CUDA is available if !Device::cuda_if_available(0).is_ok() { println!("⚠️ SKIPPED: CUDA not available, requires GPU"); return Ok(()); } // 1. Measure baseline GPU memory (no model loaded) let baseline = measure_baseline_memory()?; // 2. Measure F32 TFT memory let (f32_memory_mb, _f32_model) = measure_f32_memory(&baseline)?; // 3. Measure INT8 TFT memory let (int8_memory_mb, mut int8_model) = measure_int8_memory(&baseline)?; // 4. Check for memory leaks (10 inferences) let leak_measurements = check_memory_leaks(&mut int8_model, &baseline)?; // 5. Calculate reduction metrics let reduction_ratio = f32_memory_mb / int8_memory_mb; let reduction_percent = ((f32_memory_mb - int8_memory_mb) / f32_memory_mb) * 100.0; let meets_target = int8_memory_mb <= INT8_TARGET_MB && reduction_ratio >= MIN_REDUCTION_RATIO; // 6. Check for memory leaks let min_leak = leak_measurements.iter().cloned().fold(f64::INFINITY, f64::min); let max_leak = leak_measurements.iter().cloned().fold(f64::NEG_INFINITY, f64::max); let leak_range = max_leak - min_leak; let no_leaks = leak_range <= MEMORY_LEAK_TOLERANCE_MB; // 7. Generate report let report = MemoryBenchmarkReport { baseline_mb: baseline.memory_used_mb, f32_memory_mb, int8_memory_mb, reduction_ratio, reduction_percent, meets_target, no_leaks, leak_measurements, }; report.print_summary(); // 8. Assert test conditions assert!( meets_target, "INT8 memory {:.0} MB exceeds target {:.0} MB (reduction ratio {:.2}x < {:.1}x required)", int8_memory_mb, INT8_TARGET_MB, reduction_ratio, MIN_REDUCTION_RATIO ); assert!( no_leaks, "Memory leak detected: {:.0} MB growth exceeds tolerance {:.0} MB", leak_range, MEMORY_LEAK_TOLERANCE_MB ); println!("✅ All memory benchmark tests passed!"); println!(" - INT8 memory: {:.0} MB (target: <{:.0} MB)", int8_memory_mb, INT8_TARGET_MB); println!(" - Reduction: {:.1}% ({:.2}x)", reduction_percent, reduction_ratio); println!(" - No leaks: {:.0} MB range over {} inferences", leak_range, NUM_LEAK_CHECKS); Ok(()) } #[test] #[serial_test::serial] fn test_f32_baseline_memory() -> Result<(), MLError> { println!("\n📊 Testing F32 baseline memory measurement..."); if !Device::cuda_if_available(0).is_ok() { println!("⚠️ SKIPPED: CUDA not available"); return Ok(()); } let baseline = measure_baseline_memory()?; let (f32_memory_mb, _model) = measure_f32_memory(&baseline)?; // Validate F32 memory is positive and reasonable assert!( f32_memory_mb > 0.0, "F32 memory must be positive, got {:.0} MB", f32_memory_mb ); assert!( f32_memory_mb < 4096.0, "F32 memory {:.0} MB exceeds GPU capacity 4096 MB", f32_memory_mb ); println!("✅ F32 baseline memory: {:.0} MB", f32_memory_mb); Ok(()) } #[test] #[serial_test::serial] fn test_int8_memory_reduction() -> Result<(), MLError> { println!("\n📊 Testing INT8 memory reduction..."); if !Device::cuda_if_available(0).is_ok() { println!("⚠️ SKIPPED: CUDA not available"); return Ok(()); } let baseline = measure_baseline_memory()?; let (f32_memory_mb, _f32_model) = measure_f32_memory(&baseline)?; let (int8_memory_mb, _int8_model) = measure_int8_memory(&baseline)?; let reduction_ratio = f32_memory_mb / int8_memory_mb; let reduction_percent = ((f32_memory_mb - int8_memory_mb) / f32_memory_mb) * 100.0; println!(" F32 Memory: {:.0} MB", f32_memory_mb); println!(" INT8 Memory: {:.0} MB", int8_memory_mb); println!(" Reduction: {:.1}% ({:.2}x)", reduction_percent, reduction_ratio); // INT8 must use less memory than F32 assert!( int8_memory_mb < f32_memory_mb, "INT8 memory {:.0} MB must be less than F32 memory {:.0} MB", int8_memory_mb, f32_memory_mb ); // Must achieve at least 4x reduction assert!( reduction_ratio >= MIN_REDUCTION_RATIO, "Reduction ratio {:.2}x is below required {:.1}x", reduction_ratio, MIN_REDUCTION_RATIO ); println!("✅ INT8 achieves {:.2}x reduction (target: {:.1}x)", reduction_ratio, MIN_REDUCTION_RATIO); Ok(()) } #[test] #[serial_test::serial] fn test_int8_memory_threshold() -> Result<(), MLError> { println!("\n📊 Testing INT8 memory threshold <{:.0} MB...", INT8_TARGET_MB); if !Device::cuda_if_available(0).is_ok() { println!("⚠️ SKIPPED: CUDA not available"); return Ok(()); } let baseline = measure_baseline_memory()?; let (int8_memory_mb, _model) = measure_int8_memory(&baseline)?; println!(" INT8 Memory: {:.0} MB (target: <{:.0} MB)", int8_memory_mb, INT8_TARGET_MB); assert!( int8_memory_mb <= INT8_TARGET_MB, "INT8 memory {:.0} MB exceeds target {:.0} MB", int8_memory_mb, INT8_TARGET_MB ); let headroom_mb = INT8_TARGET_MB - int8_memory_mb; println!("✅ INT8 memory {:.0} MB is below target with {:.0} MB headroom", int8_memory_mb, headroom_mb); Ok(()) } #[test] #[serial_test::serial] fn test_no_memory_leaks() -> Result<(), MLError> { println!("\n📊 Testing for memory leaks ({} inferences)...", NUM_LEAK_CHECKS); if !Device::cuda_if_available(0).is_ok() { println!("⚠️ SKIPPED: CUDA not available"); return Ok(()); } let baseline = measure_baseline_memory()?; let (_int8_memory_mb, mut model) = measure_int8_memory(&baseline)?; let measurements = check_memory_leaks(&mut model, &baseline)?; let min_mem = measurements.iter().cloned().fold(f64::INFINITY, f64::min); let max_mem = measurements.iter().cloned().fold(f64::NEG_INFINITY, f64::max); let leak_range = max_mem - min_mem; println!(" Memory Range: {:.0} MB (tolerance: {:.0} MB)", leak_range, MEMORY_LEAK_TOLERANCE_MB); assert!( leak_range <= MEMORY_LEAK_TOLERANCE_MB, "Memory leak detected: {:.0} MB growth exceeds tolerance {:.0} MB", leak_range, MEMORY_LEAK_TOLERANCE_MB ); println!("✅ No memory leaks detected ({:.0} MB variation over {} inferences)", leak_range, NUM_LEAK_CHECKS); Ok(()) }