Skip to main content

torsh_utils/
benchmark.rs

1//! # Model Performance Benchmarking
2//!
3//! This module provides comprehensive benchmarking utilities for measuring and analyzing
4//! model performance, including inference speed, memory usage, and mobile-specific metrics.
5//!
6//! ## Features
7//!
8//! - **Multi-batch Benchmarking**: Test performance across different batch sizes
9//! - **Memory Profiling**: Track memory allocation and usage
10//! - **Backward Pass Profiling**: Measure training performance
11//! - **Mobile Benchmarking**: Platform-specific performance validation
12//! - **Statistical Analysis**: Mean, std dev, percentiles (p95, p99)
13//! - **Throughput Metrics**: Samples per second calculation
14//!
15//! ## Quick Start
16//!
17//! ```rust,no_run
18//! use torsh_utils::benchmark::{benchmark_model, BenchmarkConfig, print_benchmark_results};
19//! # use torsh_nn::Module;
20//!
21//! # struct MyModel;
22//! # impl Module for MyModel {
23//! #    fn forward(&self, _input: &torsh_tensor::Tensor) -> Result<torsh_tensor::Tensor, torsh_core::TorshError> {
24//! #       unimplemented!()
25//! #    }
26//! # }
27//!
28//! # fn example() -> Result<(), torsh_core::TorshError> {
29//! let model = MyModel;
30//!
31//! // Configure benchmarking
32//! let config = BenchmarkConfig {
33//!     warmup_iterations: 10,
34//!     benchmark_iterations: 100,
35//!     batch_sizes: vec![1, 8, 16, 32],
36//!     input_shapes: vec![vec![3, 224, 224]],
37//!     profile_memory: true,
38//!     profile_backward: true,
39//!     device: torsh_core::DeviceType::Cpu,
40//!     mobile_config: None,
41//! };
42//!
43//! // Run benchmark
44//! let results = benchmark_model(&model, config)?;
45//!
46//! // Print results
47//! print_benchmark_results(&results);
48//! # Ok(())
49//! # }
50//! ```
51//!
52//! ## Mobile Benchmarking
53//!
54//! Test model performance on mobile platforms with specific configurations:
55//!
56//! ```rust,ignore
57//! use torsh_utils::benchmark::{BenchmarkConfig, MobileBenchmarkConfig, PlatformBenchmarkInfo, MobilePlatform};
58//! # use torsh_nn::Module;
59//!
60//! # struct MyModel;
61//! # impl Module for MyModel {
62//! #    fn forward(&self, _input: &torsh_tensor::Tensor) -> Result<torsh_tensor::Tensor, torsh_core::TorshError> {
63//! #       unimplemented!()
64//! #    }
65//! # }
66//! # fn example() -> Result<(), torsh_core::TorshError> {
67//! let mobile_config = MobileBenchmarkConfig {
68//!     platform_info: PlatformBenchmarkInfo {
69//!         platform: MobilePlatform::iOS {
70//!             device: "iPhone 15 Pro".to_string(),
71//!             ios_version: "17.0".to_string(),
72//!         },
73//!         chip: "A17 Pro".to_string(),
74//!         cores: 6,
75//!         gpu_cores: Some(6),
76//!         ram_gb: 8.0,
77//!     },
78//!     monitor_thermal: true,
79//!     measure_power: true,
80//!     test_frequency_scaling: false,
81//!     test_memory_pressure: true,
82//!     stress_test_duration_minutes: Some(5),
83//!     latency_thresholds: Default::default(),
84//!     energy_targets: Some(Default::default()),
85//! };
86//!
87//! let config = BenchmarkConfig {
88//!     mobile_config: Some(mobile_config),
89//!     ..Default::default()
90//! };
91//! # Ok(())
92//! # }
93//! ```
94//!
95//! ## Understanding Results
96//!
97//! Benchmark results include detailed statistics:
98//!
99//! - **Timing Statistics**: Mean, std dev, min, max, median, p95, p99
100//! - **Throughput**: Samples processed per second
101//! - **Memory**: Peak and average memory usage
102//! - **Recommendations**: Automatic performance optimization suggestions
103//!
104//! ## Best Practices
105//!
106//! 1. **Warmup**: Always include warmup iterations to avoid cold start overhead
107//! 2. **Representative Workload**: Use realistic input sizes and batch sizes
108//! 3. **Multiple Runs**: Run benchmarks multiple times for statistical significance
109//! 4. **Isolation**: Minimize background processes during benchmarking
110//! 5. **Mobile Testing**: Test on actual target devices, not just simulators
111//!
112//! ## Performance Tips
113//!
114//! - Use larger batch sizes for higher throughput (up to memory limits)
115//! - Consider batch size impact on latency vs throughput trade-off
116//! - Monitor memory usage to avoid OOM errors
117//! - Profile both forward and backward passes for training workloads
118
119use std::collections::HashMap;
120use std::time::{Duration, Instant};
121use torsh_core::error::Result;
122use torsh_nn::Module;
123
124use crate::mobile_optimizer::{
125    MobileBenchmarkResults, MobilePlatform, OptimizedModel, PlatformBenchmarkInfo, ThermalState,
126};
127
128/// Benchmark configuration
129#[derive(Debug, Clone)]
130pub struct BenchmarkConfig {
131    pub warmup_iterations: usize,
132    pub benchmark_iterations: usize,
133    pub batch_sizes: Vec<usize>,
134    pub input_shapes: Vec<Vec<usize>>,
135    pub profile_memory: bool,
136    pub profile_backward: bool,
137    pub device: torsh_core::DeviceType,
138    pub mobile_config: Option<MobileBenchmarkConfig>,
139}
140
141/// Mobile-specific benchmark configuration
142#[derive(Debug, Clone)]
143pub struct MobileBenchmarkConfig {
144    /// Platform information for mobile benchmarking
145    pub platform_info: PlatformBenchmarkInfo,
146    /// Enable thermal monitoring
147    pub monitor_thermal: bool,
148    /// Enable power consumption measurement
149    pub measure_power: bool,
150    /// Test different CPU/GPU frequency settings
151    pub test_frequency_scaling: bool,
152    /// Test under memory pressure
153    pub test_memory_pressure: bool,
154    /// Stress test for sustained performance
155    pub stress_test_duration_minutes: Option<u32>,
156    /// Target latency thresholds for validation
157    pub latency_thresholds: LatencyThresholds,
158    /// Energy efficiency targets
159    pub energy_targets: Option<EnergyTargets>,
160}
161
162/// Latency thresholds for mobile validation
163#[derive(Debug, Clone)]
164pub struct LatencyThresholds {
165    /// Real-time inference threshold (ms)
166    pub realtime_ms: f32,
167    /// Interactive threshold (ms)
168    pub interactive_ms: f32,
169    /// Batch processing threshold (ms)
170    pub batch_ms: f32,
171}
172
173impl Default for LatencyThresholds {
174    fn default() -> Self {
175        Self {
176            realtime_ms: 16.67,    // 60 FPS
177            interactive_ms: 100.0, // 100ms for interactive
178            batch_ms: 1000.0,      // 1 second for batch
179        }
180    }
181}
182
183/// Energy efficiency targets
184#[derive(Debug, Clone)]
185pub struct EnergyTargets {
186    /// Target inferences per joule
187    pub inferences_per_joule: f32,
188    /// Maximum power consumption (watts)
189    pub max_power_watts: f32,
190    /// Target battery life hours for continuous inference
191    pub target_battery_hours: f32,
192}
193
194impl Default for EnergyTargets {
195    fn default() -> Self {
196        Self {
197            inferences_per_joule: 1000.0,
198            max_power_watts: 5.0,
199            target_battery_hours: 8.0,
200        }
201    }
202}
203
204impl Default for BenchmarkConfig {
205    fn default() -> Self {
206        Self {
207            warmup_iterations: 10,
208            benchmark_iterations: 100,
209            batch_sizes: vec![1, 8, 16, 32],
210            input_shapes: vec![vec![3, 224, 224]],
211            profile_memory: true,
212            profile_backward: true,
213            device: torsh_core::DeviceType::Cpu,
214            mobile_config: None,
215        }
216    }
217}
218
219/// Benchmark result
220#[derive(Debug, Clone)]
221pub struct BenchmarkResult {
222    pub model_name: String,
223    pub total_params: usize,
224    pub results_by_batch: HashMap<usize, BatchResult>,
225    pub summary: BenchmarkSummary,
226    pub mobile_results: Option<MobileBenchmarkResults>,
227    pub validation_results: Option<ValidationResults>,
228}
229
230/// Validation results for mobile deployment
231#[derive(Debug, Clone)]
232pub struct ValidationResults {
233    /// Whether model meets real-time latency requirements
234    pub meets_realtime_latency: bool,
235    /// Whether model meets interactive latency requirements
236    pub meets_interactive_latency: bool,
237    /// Whether model meets energy efficiency targets
238    pub meets_energy_targets: bool,
239    /// Thermal throttling detected during testing
240    pub thermal_throttling_detected: bool,
241    /// Memory pressure impact on performance
242    pub memory_pressure_impact: Option<f32>,
243    /// Sustained performance degradation percentage
244    pub sustained_performance_degradation: Option<f32>,
245    /// Platform-specific validation results
246    pub platform_validation: PlatformValidationResults,
247    /// Recommendations for improvement
248    pub recommendations: Vec<String>,
249}
250
251/// Platform-specific validation results
252#[derive(Debug, Clone)]
253pub struct PlatformValidationResults {
254    /// iOS App Store guidelines compliance
255    pub ios_app_store_compliant: Option<bool>,
256    /// Android performance class requirements
257    pub android_performance_class: Option<String>,
258    /// Device compatibility score (0-100)
259    pub device_compatibility_score: f32,
260    /// Estimated device support percentage
261    pub device_support_percentage: f32,
262}
263
264/// Results for a specific batch size
265#[derive(Debug, Clone)]
266pub struct BatchResult {
267    pub batch_size: usize,
268    pub forward_time: TimingStats,
269    pub backward_time: Option<TimingStats>,
270    pub total_time: TimingStats,
271    pub throughput: f32,
272    pub memory_stats: Option<MemoryStats>,
273}
274
275/// Timing statistics
276#[derive(Debug, Clone)]
277pub struct TimingStats {
278    pub mean: Duration,
279    pub std: Duration,
280    pub min: Duration,
281    pub max: Duration,
282    pub median: Duration,
283    pub p95: Duration,
284    pub p99: Duration,
285}
286
287/// Memory statistics
288#[derive(Debug, Clone)]
289pub struct MemoryStats {
290    pub peak_allocated_mb: f32,
291    pub peak_reserved_mb: f32,
292    pub avg_allocated_mb: f32,
293}
294
295/// Benchmark summary
296#[derive(Debug, Clone)]
297pub struct BenchmarkSummary {
298    pub best_batch_size: usize,
299    pub best_throughput: f32,
300    pub optimal_memory_batch: usize,
301    pub recommendations: Vec<String>,
302}
303
304/// Benchmark a model with optional mobile-specific testing
305pub fn benchmark_model<M: Module>(model: &M, config: BenchmarkConfig) -> Result<BenchmarkResult> {
306    let model_name = std::any::type_name::<M>()
307        .split("::")
308        .last()
309        .unwrap_or("UnknownModel")
310        .to_string();
311
312    let total_params = count_parameters(model);
313    let mut results_by_batch = HashMap::new();
314
315    for batch_size in &config.batch_sizes {
316        println!("Benchmarking batch size: {}", batch_size);
317
318        let result = benchmark_batch_size(model, *batch_size, &config.input_shapes[0], &config)?;
319
320        results_by_batch.insert(*batch_size, result);
321    }
322
323    let summary = generate_summary(&results_by_batch);
324
325    // Perform mobile-specific benchmarking if configured
326    let mobile_results = if let Some(mobile_config) = &config.mobile_config {
327        Some(benchmark_mobile_model(model, mobile_config)?)
328    } else {
329        None
330    };
331
332    // Perform validation if mobile benchmarking was done. The three
333    // additional mobile sub-tests run here (rather than inside
334    // `benchmark_mobile_model`) so their real measured results can be
335    // written directly onto the `ValidationResults` fields that report
336    // them.
337    let validation_results = if let Some(mobile_config) = &config.mobile_config {
338        let mut validation =
339            validate_mobile_performance(&results_by_batch, mobile_config, mobile_results.as_ref())?;
340
341        // Same convention as `benchmark_batch_size`: prepend a batch size
342        // of 1 to the caller's configured (batch-less) input shape.
343        let mut mobile_input_shape = vec![1usize];
344        mobile_input_shape.extend_from_slice(&config.input_shapes[0]);
345
346        if mobile_config.test_memory_pressure {
347            validation.memory_pressure_impact =
348                run_memory_pressure_test(model, &mobile_input_shape, mobile_config)?;
349        }
350
351        if mobile_config.test_frequency_scaling {
352            // Frequency-scaling control cannot be portably measured in
353            // pure Rust (see `run_frequency_scaling_test`); report it as
354            // skipped rather than failing the entire benchmark run over
355            // one orthogonal, best-effort sub-test.
356            if let Err(e) = run_frequency_scaling_test(model, mobile_config) {
357                validation
358                    .recommendations
359                    .push(format!("Frequency scaling test skipped: {e}"));
360            }
361        }
362
363        if let Some(duration) = mobile_config.stress_test_duration_minutes {
364            validation.sustained_performance_degradation = run_sustained_performance_test(
365                model,
366                &mobile_input_shape,
367                mobile_config,
368                duration,
369            )?;
370        }
371
372        Some(validation)
373    } else {
374        None
375    };
376
377    Ok(BenchmarkResult {
378        model_name,
379        total_params,
380        results_by_batch,
381        summary,
382        mobile_results,
383        validation_results,
384    })
385}
386
387/// Benchmark model specifically for mobile deployment
388pub fn benchmark_mobile_model<M: Module>(
389    model: &M,
390    mobile_config: &MobileBenchmarkConfig,
391) -> Result<MobileBenchmarkResults> {
392    use crate::mobile_optimizer::benchmark_mobile_model_advanced;
393
394    println!("Running mobile-specific benchmark...");
395
396    // Convert model to optimized representation for benchmarking
397    // This is simplified - in practice would extract actual model structure
398    let optimized_model = convert_to_optimized_model(model)?;
399
400    // Input shapes for mobile benchmarking (typically smaller batches)
401    let input_shapes = vec![vec![1, 3, 224, 224]]; // Mobile-typical input
402
403    // Run comprehensive mobile benchmark
404    let mobile_results = benchmark_mobile_model_advanced(
405        &optimized_model,
406        input_shapes,
407        mobile_config.stress_test_duration_minutes.unwrap_or(5) as usize * 60, // Convert to iterations
408        &mobile_config.platform_info,
409    );
410
411    // Memory-pressure, frequency-scaling, and sustained-performance testing
412    // run in `benchmark_model` instead of here: that caller also builds
413    // `ValidationResults`, so their real measured outputs can be written
414    // directly onto `memory_pressure_impact` / `sustained_performance_degradation`
415    // there rather than being computed here and then discarded.
416
417    Ok(mobile_results)
418}
419
420/// Validate mobile performance against requirements
421pub fn validate_mobile_performance(
422    batch_results: &HashMap<usize, BatchResult>,
423    mobile_config: &MobileBenchmarkConfig,
424    mobile_results: Option<&MobileBenchmarkResults>,
425) -> Result<ValidationResults> {
426    let thresholds = &mobile_config.latency_thresholds;
427
428    // Check latency requirements (use batch size 1 for mobile)
429    let batch_1_result = batch_results.get(&1);
430    let meets_realtime = batch_1_result
431        .map(|r| r.total_time.mean.as_millis() as f32 <= thresholds.realtime_ms)
432        .unwrap_or(false);
433
434    let meets_interactive = batch_1_result
435        .map(|r| r.total_time.mean.as_millis() as f32 <= thresholds.interactive_ms)
436        .unwrap_or(false);
437
438    // Check energy efficiency if targets are set
439    let meets_energy = if let (Some(targets), Some(mobile_res)) =
440        (&mobile_config.energy_targets, mobile_results)
441    {
442        mobile_res
443            .detailed_metrics
444            .energy_efficiency
445            .map(|eff| eff >= targets.inferences_per_joule / 1000.0) // Convert to per mW
446            .unwrap_or(false)
447    } else {
448        true // No targets set, consider as met
449    };
450
451    // Check thermal throttling
452    let thermal_throttling = mobile_results
453        .map(|r| {
454            matches!(
455                r.detailed_metrics.thermal_state,
456                ThermalState::Hot | ThermalState::Critical
457            )
458        })
459        .unwrap_or(false);
460
461    // Platform-specific validation
462    let platform_validation = validate_platform_requirements(&mobile_config.platform_info);
463
464    // Generate recommendations
465    let mut recommendations = Vec::new();
466
467    if !meets_realtime {
468        recommendations.push(
469            "Model latency exceeds real-time requirements. Consider quantization or pruning."
470                .to_string(),
471        );
472    }
473
474    if !meets_interactive {
475        recommendations.push(
476            "Model latency exceeds interactive requirements. Optimize critical path operations."
477                .to_string(),
478        );
479    }
480
481    if !meets_energy {
482        recommendations.push("Model energy efficiency below target. Consider lower precision or architectural changes.".to_string());
483    }
484
485    if thermal_throttling {
486        recommendations.push(
487            "Thermal throttling detected. Reduce computational intensity or add thermal breaks."
488                .to_string(),
489        );
490    }
491
492    Ok(ValidationResults {
493        meets_realtime_latency: meets_realtime,
494        meets_interactive_latency: meets_interactive,
495        meets_energy_targets: meets_energy,
496        thermal_throttling_detected: thermal_throttling,
497        memory_pressure_impact: None, // Would be set by memory pressure test
498        sustained_performance_degradation: None, // Would be set by sustained test
499        platform_validation,
500        recommendations,
501    })
502}
503
504/// Convert module to optimized model representation for benchmarking.
505///
506/// This is an identity conversion: the returned [`ModelGraph`] is empty and
507/// `weights` is empty because this crate has no generic way to extract a
508/// layer graph from an arbitrary [`Module`] (the trait exposes no such
509/// method). What genuinely *is* derived from `model` is its total
510/// parameter byte size -- `numel * dtype size`, summed recursively over
511/// every parameter including submodules via [`Module::all_parameters`] --
512/// real data, replacing the fixed 10MB/8MB placeholder this used to report
513/// for every model regardless of its actual size.
514///
515/// Because no optimization pass actually runs here, `optimized_size`
516/// equals `original_size` (nothing was compressed) and
517/// `compression_ratio`/`estimated_speedup` are the true values for an
518/// identity transform (`1.0` each) rather than invented numbers.
519fn convert_to_optimized_model<M: Module>(model: &M) -> Result<OptimizedModel> {
520    use crate::mobile_optimizer::{ModelGraph, OptimizationMetadata};
521
522    let total_bytes: usize = model
523        .all_parameters()
524        .values()
525        .map(|param| {
526            let tensor = param.tensor();
527            let tensor = tensor.read();
528            tensor.numel() * tensor.dtype().size_bytes()
529        })
530        .sum();
531
532    Ok(OptimizedModel {
533        graph: ModelGraph {
534            nodes: vec![],
535            edges: vec![],
536            inputs: vec![],
537            outputs: vec![],
538        },
539        weights: HashMap::new(),
540        metadata: OptimizationMetadata {
541            original_size: total_bytes,
542            optimized_size: total_bytes,
543            // original_size == optimized_size always here (no compression
544            // actually happened), so 1.0 is the true ratio -- not computed
545            // via a division that could divide zero by zero for a
546            // parameter-less model.
547            compression_ratio: 1.0,
548            applied_passes: vec!["benchmark_conversion".to_string()],
549            estimated_speedup: 1.0,
550            backend_metadata: HashMap::new(),
551        },
552        backend_data: None,
553    })
554}
555
556/// Run `samples` real forward passes of `model` and return each one's real
557/// wall-clock duration in milliseconds.
558fn time_forward_passes<M: Module>(
559    model: &M,
560    input_shape: &[usize],
561    samples: usize,
562) -> Result<Vec<f32>> {
563    let mut latencies_ms = Vec::with_capacity(samples);
564    for _ in 0..samples {
565        let input = torsh_tensor::creation::randn(input_shape)?;
566        let start = Instant::now();
567        let _ = model.forward(&input)?;
568        latencies_ms.push(start.elapsed().as_secs_f32() * 1000.0);
569    }
570    Ok(latencies_ms)
571}
572
573/// Arithmetic mean, or `0.0` for an empty slice.
574fn mean(values: &[f32]) -> f32 {
575    if values.is_empty() {
576        0.0
577    } else {
578        values.iter().sum::<f32>() / values.len() as f32
579    }
580}
581
582/// Run a real memory-pressure test: measure how much slower `model`'s
583/// forward pass becomes while a large, genuinely-committed allocation
584/// competes for memory, compared to an unpressured baseline.
585///
586/// Returns `Ok(None)` only if too few timing samples were usable to compute
587/// a meaningful comparison (e.g. all baseline latencies were reported as
588/// zero) -- never a fabricated placeholder percentage. `_config` is
589/// currently unused (the pressure amount is a fixed, modest constant) but
590/// kept for API stability and future tuning.
591fn run_memory_pressure_test<M: Module>(
592    model: &M,
593    input_shape: &[usize],
594    _config: &MobileBenchmarkConfig,
595) -> Result<Option<f32>> {
596    const SAMPLES: usize = 8;
597    // 64 MiB: enough to create real memory pressure without being an
598    // antisocial allocation on a machine that may be shared with other
599    // concurrent work.
600    const PRESSURE_BYTES: usize = 64 * 1024 * 1024;
601
602    let baseline = time_forward_passes(model, input_shape, SAMPLES)?;
603
604    // Force real page commitment: a freshly-zeroed `vec![0u8; N]` can stay
605    // backed by a single shared zero page on some allocators/OSes until
606    // written, which would measure noise while claiming to measure memory
607    // pressure. Writing one byte per 4 KiB page (the smallest common page
608    // size) guarantees every page is actually resident.
609    let mut pressure = vec![0u8; PRESSURE_BYTES];
610    for page in pressure.chunks_mut(4096) {
611        page[0] = 1;
612    }
613
614    let pressured = time_forward_passes(model, input_shape, SAMPLES)?;
615    drop(pressure);
616
617    let baseline_mean_ms = mean(&baseline);
618    let pressured_mean_ms = mean(&pressured);
619    if baseline_mean_ms <= 0.0 {
620        return Ok(None);
621    }
622
623    Ok(Some(
624        (pressured_mean_ms - baseline_mean_ms) / baseline_mean_ms * 100.0,
625    ))
626}
627
628/// CPU/GPU frequency-scaling control (e.g. Linux cpufreq governors) requires
629/// privileged, platform-specific system APIs this crate does not have --
630/// portably reading or writing them needs root and/or FFI, which is outside
631/// this crate's pure-Rust, unprivileged default. Returns an honest error
632/// rather than the fabricated `Ok(())` (a printed line and no actual test)
633/// this used to unconditionally report; callers should treat this as
634/// "frequency scaling testing is unavailable", not a real pass/fail result.
635fn run_frequency_scaling_test<M: Module>(
636    _model: &M,
637    _config: &MobileBenchmarkConfig,
638) -> Result<()> {
639    Err(torsh_core::TorshError::NotImplemented(
640        "CPU/GPU frequency-scaling control requires privileged, platform-specific system APIs \
641         not available in pure Rust"
642            .to_string(),
643    ))
644}
645
646/// Run a real sustained-load test: repeatedly execute `model`'s forward
647/// pass and compare the mean latency of the first half of the collected
648/// samples against the second half, to detect real performance
649/// degradation over time (e.g. thermal throttling) -- instead of the
650/// fabricated `None` this used to unconditionally report after printing a
651/// line and doing no actual work.
652///
653/// Bounded by both `duration_minutes` (the caller's requested wall-clock
654/// budget) and a hard sample cap, whichever is reached first, so a
655/// pathological configuration (a very fast model paired with a very long
656/// requested duration) cannot run unbounded in an automated context. At
657/// least a small minimum number of samples is always collected (even for
658/// `duration_minutes == 0`) so a real, if brief, comparison can still be
659/// made.
660fn run_sustained_performance_test<M: Module>(
661    model: &M,
662    input_shape: &[usize],
663    _config: &MobileBenchmarkConfig,
664    duration_minutes: u32,
665) -> Result<Option<f32>> {
666    const MIN_SAMPLES: usize = 8;
667    const MAX_SAMPLES: usize = 100_000;
668
669    let budget = Duration::from_secs(u64::from(duration_minutes) * 60);
670    let start = Instant::now();
671    let mut latencies_ms = Vec::new();
672
673    loop {
674        let input = torsh_tensor::creation::randn(input_shape)?;
675        let sample_start = Instant::now();
676        let _ = model.forward(&input)?;
677        latencies_ms.push(sample_start.elapsed().as_secs_f32() * 1000.0);
678
679        let min_met = latencies_ms.len() >= MIN_SAMPLES;
680        let time_up = start.elapsed() >= budget;
681        let hit_cap = latencies_ms.len() >= MAX_SAMPLES;
682        if (min_met && time_up) || hit_cap {
683            break;
684        }
685    }
686
687    if latencies_ms.len() < 2 {
688        return Ok(None);
689    }
690
691    let half = latencies_ms.len() / 2;
692    let first_half_mean = mean(&latencies_ms[..half]);
693    let second_half_mean = mean(&latencies_ms[half..]);
694    if first_half_mean <= 0.0 {
695        return Ok(None);
696    }
697
698    Ok(Some(
699        (second_half_mean - first_half_mean) / first_half_mean * 100.0,
700    ))
701}
702
703/// Validate platform-specific requirements
704fn validate_platform_requirements(
705    platform_info: &PlatformBenchmarkInfo,
706) -> PlatformValidationResults {
707    match &platform_info.platform {
708        MobilePlatform::iOS { .. } => PlatformValidationResults {
709            ios_app_store_compliant: Some(true), // Would check actual guidelines
710            android_performance_class: None,
711            device_compatibility_score: 85.0,
712            device_support_percentage: 95.0,
713        },
714        MobilePlatform::Android { .. } => PlatformValidationResults {
715            ios_app_store_compliant: None,
716            android_performance_class: Some("T".to_string()), // Tier classification
717            device_compatibility_score: 80.0,
718            device_support_percentage: 90.0,
719        },
720        MobilePlatform::Other(_) => PlatformValidationResults {
721            ios_app_store_compliant: None,
722            android_performance_class: None,
723            device_compatibility_score: 70.0,
724            device_support_percentage: 75.0,
725        },
726    }
727}
728
729/// Benchmark a specific batch size
730fn benchmark_batch_size<M: Module>(
731    model: &M,
732    batch_size: usize,
733    base_shape: &[usize],
734    config: &BenchmarkConfig,
735) -> Result<BatchResult> {
736    let mut input_shape = vec![batch_size];
737    input_shape.extend_from_slice(base_shape);
738
739    let mut forward_times = Vec::new();
740    let mut backward_times = Vec::new();
741    let mut memory_samples = Vec::new();
742
743    // Warmup
744    for _ in 0..config.warmup_iterations {
745        let input = torsh_tensor::creation::randn(&input_shape)?;
746        let _ = model.forward(&input)?;
747    }
748
749    // Benchmark
750    let benchmark_start = Instant::now();
751
752    for _ in 0..config.benchmark_iterations {
753        let input = torsh_tensor::creation::randn(&input_shape)?;
754
755        // Forward pass
756        let forward_start = Instant::now();
757        let output = model.forward(&input)?;
758        let forward_time = forward_start.elapsed();
759        forward_times.push(forward_time);
760
761        // Backward pass if requested
762        if config.profile_backward && output.requires_grad() {
763            let backward_start = Instant::now();
764            output.sum()?.backward()?;
765            let backward_time = backward_start.elapsed();
766            backward_times.push(backward_time);
767        }
768
769        // Memory profiling
770        if config.profile_memory {
771            if let Ok((allocated, reserved)) = get_current_memory() {
772                memory_samples.push((allocated, reserved));
773            }
774        }
775    }
776
777    let _total_time = benchmark_start.elapsed();
778
779    // Calculate statistics
780    let forward_stats = calculate_timing_stats(&forward_times);
781    let backward_stats = if !backward_times.is_empty() {
782        Some(calculate_timing_stats(&backward_times))
783    } else {
784        None
785    };
786
787    let total_times: Vec<Duration> = forward_times
788        .iter()
789        .zip(
790            backward_times
791                .iter()
792                .chain(std::iter::repeat(&Duration::ZERO)),
793        )
794        .map(|(f, b)| *f + *b)
795        .collect();
796
797    let total_stats = calculate_timing_stats(&total_times);
798
799    // Calculate throughput (samples per second)
800    let avg_time_per_sample = total_stats.mean.as_secs_f32() / batch_size as f32;
801    let throughput = 1.0 / avg_time_per_sample;
802
803    // Memory statistics
804    let memory_stats = if !memory_samples.is_empty() {
805        Some(calculate_memory_stats(&memory_samples))
806    } else {
807        None
808    };
809
810    Ok(BatchResult {
811        batch_size,
812        forward_time: forward_stats,
813        backward_time: backward_stats,
814        total_time: total_stats,
815        throughput,
816        memory_stats,
817    })
818}
819
820/// Count model parameters, recursively including submodules.
821///
822/// Uses [`Module::all_parameters`] (not the non-recursive `parameters()`)
823/// so a model built from nested submodules is not undercounted.
824fn count_parameters<M: Module>(model: &M) -> usize {
825    model
826        .all_parameters()
827        .values()
828        .map(|p| p.tensor().read().numel())
829        .sum()
830}
831
832/// Calculate timing statistics
833fn calculate_timing_stats(times: &[Duration]) -> TimingStats {
834    let mut sorted_times = times.to_vec();
835    sorted_times.sort();
836
837    let n = sorted_times.len() as f32;
838    let mean = sorted_times.iter().sum::<Duration>() / sorted_times.len() as u32;
839
840    let variance = sorted_times
841        .iter()
842        .map(|t| {
843            let diff = t.as_secs_f32() - mean.as_secs_f32();
844            diff * diff
845        })
846        .sum::<f32>()
847        / n;
848
849    let std = Duration::from_secs_f32(variance.sqrt());
850
851    TimingStats {
852        mean,
853        std,
854        min: sorted_times[0],
855        max: sorted_times[sorted_times.len() - 1],
856        median: sorted_times[sorted_times.len() / 2],
857        p95: sorted_times[(0.95 * n) as usize],
858        p99: sorted_times[(0.99 * n) as usize],
859    }
860}
861
862/// Calculate memory statistics
863fn calculate_memory_stats(samples: &[(f32, f32)]) -> MemoryStats {
864    let peak_allocated = samples.iter().map(|(a, _)| *a).fold(0.0f32, f32::max);
865    let peak_reserved = samples.iter().map(|(_, r)| *r).fold(0.0f32, f32::max);
866    let avg_allocated = samples.iter().map(|(a, _)| *a).sum::<f32>() / samples.len() as f32;
867
868    MemoryStats {
869        peak_allocated_mb: peak_allocated,
870        peak_reserved_mb: peak_reserved,
871        avg_allocated_mb: avg_allocated,
872    }
873}
874
875/// Get current memory usage (RSS and peak) from /proc/self/status.
876/// Returns (rss_mb, peak_mb). On non-Linux platforms returns (0.0, 0.0).
877fn get_current_memory() -> Result<(f32, f32)> {
878    #[cfg(target_os = "linux")]
879    {
880        let status = std::fs::read_to_string("/proc/self/status").map_err(|e| {
881            torsh_core::TorshError::IoError(format!("Failed to read /proc/self/status: {}", e))
882        })?;
883        let mut rss_kb: Option<u64> = None;
884        let mut peak_kb: Option<u64> = None;
885        for line in status.lines() {
886            if let Some(rest) = line.strip_prefix("VmRSS:") {
887                rss_kb = rest.split_whitespace().next().and_then(|s| s.parse().ok());
888            } else if let Some(rest) = line.strip_prefix("VmPeak:") {
889                peak_kb = rest.split_whitespace().next().and_then(|s| s.parse().ok());
890            }
891            if rss_kb.is_some() && peak_kb.is_some() {
892                break;
893            }
894        }
895        let rss_mb = rss_kb.unwrap_or(0) as f32 / 1024.0;
896        let peak_mb = peak_kb.unwrap_or(0) as f32 / 1024.0;
897        return Ok((rss_mb, peak_mb));
898    }
899    #[cfg(not(target_os = "linux"))]
900    {
901        // Memory measurement via /proc/self/status not available on this platform
902        Ok((0.0, 0.0))
903    }
904}
905
906/// Generate benchmark summary
907fn generate_summary(results: &HashMap<usize, BatchResult>) -> BenchmarkSummary {
908    let mut best_throughput = 0.0;
909    let mut best_batch_size = 0;
910    let mut optimal_memory_batch = 0;
911    let mut min_memory_per_sample = f32::INFINITY;
912
913    for (batch_size, result) in results {
914        if result.throughput > best_throughput {
915            best_throughput = result.throughput;
916            best_batch_size = *batch_size;
917        }
918
919        if let Some(mem) = &result.memory_stats {
920            let memory_per_sample = mem.peak_allocated_mb / *batch_size as f32;
921            if memory_per_sample < min_memory_per_sample {
922                min_memory_per_sample = memory_per_sample;
923                optimal_memory_batch = *batch_size;
924            }
925        }
926    }
927
928    let mut recommendations = Vec::new();
929
930    // Throughput recommendation
931    recommendations.push(format!(
932        "Best throughput: {:.1} samples/sec at batch size {}",
933        best_throughput, best_batch_size
934    ));
935
936    // Memory recommendation
937    if optimal_memory_batch > 0 {
938        recommendations.push(format!(
939            "Most memory efficient: batch size {} ({:.1} MB/sample)",
940            optimal_memory_batch, min_memory_per_sample
941        ));
942    }
943
944    // Scaling recommendation
945    let batch_sizes: Vec<usize> = results.keys().copied().collect();
946    if batch_sizes.len() >= 2 {
947        let min_batch = *batch_sizes.iter().min().expect("reduction should succeed");
948        let max_batch = *batch_sizes.iter().max().expect("reduction should succeed");
949
950        let min_result = &results[&min_batch];
951        let max_result = &results[&max_batch];
952
953        let scaling_efficiency =
954            (max_result.throughput * max_batch as f32) / (min_result.throughput * min_batch as f32);
955
956        if scaling_efficiency < 0.8 {
957            recommendations.push(format!(
958                "Poor scaling efficiency ({:.1}%). Consider optimizing data loading.",
959                scaling_efficiency * 100.0
960            ));
961        }
962    }
963
964    BenchmarkSummary {
965        best_batch_size,
966        best_throughput,
967        optimal_memory_batch,
968        recommendations,
969    }
970}
971
972/// Print benchmark results
973pub fn print_benchmark_results(results: &BenchmarkResult) {
974    println!("=== Benchmark Results for {} ===", results.model_name);
975    println!("Total parameters: {}", results.total_params);
976    println!();
977
978    println!(
979        "{:<10} {:<15} {:<15} {:<15} {:<15}",
980        "Batch", "Forward (ms)", "Backward (ms)", "Total (ms)", "Throughput"
981    );
982    println!("{}", "-".repeat(75));
983
984    for batch_size in results.results_by_batch.keys() {
985        let result = &results.results_by_batch[batch_size];
986        let backward_str = result
987            .backward_time
988            .as_ref()
989            .map(|t| format!("{:.2}", t.mean.as_secs_f32() * 1000.0))
990            .unwrap_or_else(|| "N/A".to_string());
991
992        println!(
993            "{:<10} {:<15.2} {:<15} {:<15.2} {:<15.1}",
994            batch_size,
995            result.forward_time.mean.as_secs_f32() * 1000.0,
996            backward_str,
997            result.total_time.mean.as_secs_f32() * 1000.0,
998            result.throughput
999        );
1000    }
1001    println!();
1002
1003    println!("Summary:");
1004    for rec in &results.summary.recommendations {
1005        println!("  - {}", rec);
1006    }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use super::*;
1012
1013    #[test]
1014    fn test_get_current_memory_nonnegative() {
1015        let (rss, peak) = get_current_memory().unwrap_or((0.0, 0.0));
1016        assert!(rss >= 0.0, "RSS should be non-negative, got {}", rss);
1017        assert!(peak >= 0.0, "peak should be non-negative, got {}", peak);
1018        #[cfg(target_os = "linux")]
1019        {
1020            assert!(rss > 0.0, "RSS should be positive on Linux, got {}", rss);
1021        }
1022    }
1023
1024    #[test]
1025    fn test_timing_stats() {
1026        let times = vec![
1027            Duration::from_millis(10),
1028            Duration::from_millis(12),
1029            Duration::from_millis(11),
1030            Duration::from_millis(13),
1031            Duration::from_millis(14),
1032        ];
1033
1034        let stats = calculate_timing_stats(&times);
1035        assert_eq!(stats.min, Duration::from_millis(10));
1036        assert_eq!(stats.max, Duration::from_millis(14));
1037        assert_eq!(stats.median, Duration::from_millis(12));
1038    }
1039
1040    // --- F173 regression tests ------------------------------------------
1041    //
1042    // `convert_to_optimized_model`, `run_memory_pressure_test`,
1043    // `run_frequency_scaling_test`, and `run_sustained_performance_test`
1044    // are private, so their real-measurement behavior is verified here
1045    // rather than from an external `tests/` integration test. See
1046    // `tests/hardening_profiler.rs` for the public-API-visible half of
1047    // F173 (the `ValidationResults` fields these feed into).
1048
1049    fn f173_test_platform_info() -> PlatformBenchmarkInfo {
1050        use crate::mobile_optimizer::{CpuInfo, MemoryInfo};
1051
1052        PlatformBenchmarkInfo {
1053            platform: MobilePlatform::iOS {
1054                chip: "A15".to_string(),
1055                neural_engine: true,
1056            },
1057            device_model: "test-device".to_string(),
1058            os_version: "1.0".to_string(),
1059            cpu_info: CpuInfo {
1060                cores_performance: 2,
1061                cores_efficiency: 4,
1062                max_frequency_ghz: 3.0,
1063                cache_l1_kb: 128,
1064                cache_l2_kb: 4096,
1065                cache_l3_kb: None,
1066            },
1067            memory_info: MemoryInfo {
1068                total_mb: 4096,
1069                bandwidth_gb_s: 30.0,
1070                memory_type: "LPDDR5".to_string(),
1071            },
1072            thermal_design_power: None,
1073        }
1074    }
1075
1076    fn f173_test_mobile_config(
1077        test_memory_pressure: bool,
1078        test_frequency_scaling: bool,
1079    ) -> MobileBenchmarkConfig {
1080        MobileBenchmarkConfig {
1081            platform_info: f173_test_platform_info(),
1082            monitor_thermal: false,
1083            measure_power: false,
1084            test_frequency_scaling,
1085            test_memory_pressure,
1086            stress_test_duration_minutes: None,
1087            latency_thresholds: LatencyThresholds::default(),
1088            energy_targets: None,
1089        }
1090    }
1091
1092    /// F173: `convert_to_optimized_model` must derive `original_size` from
1093    /// the real model passed in, not report the same fixed 10MB/8MB
1094    /// placeholder for every model regardless of its actual size.
1095    #[test]
1096    fn test_convert_to_optimized_model_uses_real_model_size() {
1097        use torsh_nn::layers::Linear;
1098
1099        let small = Linear::new(4, 4, true);
1100        let large = Linear::new(256, 256, true);
1101
1102        let small_result = convert_to_optimized_model(&small).expect("conversion should succeed");
1103        let large_result = convert_to_optimized_model(&large).expect("conversion should succeed");
1104
1105        assert_ne!(
1106            small_result.metadata.original_size, 10_000_000,
1107            "original_size must not be the historical fixed placeholder"
1108        );
1109        assert_ne!(
1110            small_result.metadata.original_size, large_result.metadata.original_size,
1111            "differently-sized real models must report different real sizes"
1112        );
1113
1114        // Linear(4, 4, bias=true): weight [4,4] + bias [4] = 20 f32 params.
1115        assert_eq!(small_result.metadata.original_size, 20 * 4);
1116        // Linear(256, 256, bias=true): weight [256,256] + bias [256] = 65_792 f32 params.
1117        assert_eq!(large_result.metadata.original_size, 65_792 * 4);
1118
1119        // No real compression pass runs in this conversion, so the honest
1120        // values for an identity transform are 1.0, not an invented guess
1121        // like the historical 1.25 / 1.2.
1122        for result in [&small_result, &large_result] {
1123            assert_eq!(
1124                result.metadata.optimized_size,
1125                result.metadata.original_size
1126            );
1127            assert_eq!(result.metadata.compression_ratio, 1.0);
1128            assert_eq!(result.metadata.estimated_speedup, 1.0);
1129        }
1130    }
1131
1132    /// F173: frequency-scaling control cannot be portably measured in pure
1133    /// Rust; this must return an honest error rather than the historical
1134    /// fabricated `Ok(())` (a printed line and no actual test).
1135    #[test]
1136    fn test_run_frequency_scaling_test_is_honest_about_being_unimplemented() {
1137        use torsh_nn::layers::Linear;
1138
1139        let model = Linear::new(4, 4, true);
1140        let config = f173_test_mobile_config(false, true);
1141
1142        let result = run_frequency_scaling_test(&model, &config);
1143        assert!(
1144            result.is_err(),
1145            "frequency scaling control must return an honest error, not a fabricated Ok(())"
1146        );
1147    }
1148
1149    /// F173: memory pressure testing must return a real, finite measured
1150    /// value derived from actually timing the model, not the historical
1151    /// `Ok(())` that discarded any notion of measurement entirely.
1152    #[test]
1153    fn test_run_memory_pressure_test_returns_finite_measured_value() {
1154        use torsh_nn::layers::Linear;
1155
1156        let model = Linear::new(8, 8, true);
1157        let config = f173_test_mobile_config(true, false);
1158
1159        let result = run_memory_pressure_test(&model, &[1, 8], &config)
1160            .expect("memory pressure test should succeed on a real, working model");
1161        let value = result
1162            .expect("enough real timing samples were collected, so a comparison must be Some(_)");
1163        assert!(
1164            value.is_finite(),
1165            "measured degradation must be a real finite number, got {value}"
1166        );
1167    }
1168
1169    /// F173: sustained-performance testing must return a real, finite
1170    /// measured value derived from actually timing the model repeatedly,
1171    /// not the historical unconditional `None`. `duration_minutes = 0`
1172    /// still takes a small minimum number of real samples, so this
1173    /// completes quickly while still being a genuine measurement.
1174    #[test]
1175    fn test_run_sustained_performance_test_returns_finite_measured_value() {
1176        use torsh_nn::layers::Linear;
1177
1178        let model = Linear::new(8, 8, true);
1179        let config = f173_test_mobile_config(false, false);
1180
1181        let result = run_sustained_performance_test(&model, &[1, 8], &config, 0)
1182            .expect("sustained performance test should succeed on a real, working model");
1183        let value = result.expect("enough real timing samples were collected for a comparison");
1184        assert!(
1185            value.is_finite(),
1186            "measured degradation must be a real finite number, got {value}"
1187        );
1188    }
1189}