Files
rustytorch/demos/inference-profiler-shared/src/metrics.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

472 lines
15 KiB
Rust

//! Performance metrics for inference profiling.
use serde::{Deserialize, Serialize};
/// Latency statistics from benchmark measurements.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LatencyMetrics {
/// Mean latency in milliseconds
pub mean_ms: f64,
/// Standard deviation in milliseconds
pub std_ms: f64,
/// Minimum latency in milliseconds
pub min_ms: f64,
/// Maximum latency in milliseconds
pub max_ms: f64,
/// 50th percentile (median) in milliseconds
pub p50_ms: f64,
/// 95th percentile in milliseconds
pub p95_ms: f64,
/// 99th percentile in milliseconds
pub p99_ms: f64,
}
impl LatencyMetrics {
/// Creates latency metrics from a collection of measurements.
///
/// # Arguments
/// * `measurements_ms` - Latency measurements in milliseconds
///
/// # Errors
/// Returns an error if measurements are empty or contain invalid values.
pub fn from_measurements(measurements_ms: &[f64]) -> Result<Self, String> {
if measurements_ms.is_empty() {
return Err("measurements cannot be empty".to_string());
}
for &m in measurements_ms {
if !m.is_finite() || m < 0.0 {
return Err(format!("invalid measurement: {m}"));
}
}
let mut sorted = measurements_ms.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mean = sorted.iter().sum::<f64>() / sorted.len() as f64;
let variance =
sorted.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / sorted.len() as f64;
let std = variance.sqrt();
let min = sorted[0];
let max = sorted[sorted.len() - 1];
let p50 = percentile(&sorted, 50.0);
let p95 = percentile(&sorted, 95.0);
let p99 = percentile(&sorted, 99.0);
Ok(Self {
mean_ms: mean,
std_ms: std,
min_ms: min,
max_ms: max,
p50_ms: p50,
p95_ms: p95,
p99_ms: p99,
})
}
}
/// Memory usage statistics.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MemoryMetrics {
/// Peak memory usage in megabytes
pub peak_memory_mb: f64,
/// Currently allocated memory in megabytes
pub allocated_mb: f64,
/// Reserved memory in megabytes (for GPU)
pub reserved_mb: f64,
}
impl MemoryMetrics {
/// Creates new memory metrics.
///
/// # Errors
/// Returns an error if any value is negative or non-finite.
pub fn new(peak_memory_mb: f64, allocated_mb: f64, reserved_mb: f64) -> Result<Self, String> {
if !peak_memory_mb.is_finite() || peak_memory_mb < 0.0 {
return Err(format!("invalid peak_memory_mb: {peak_memory_mb}"));
}
if !allocated_mb.is_finite() || allocated_mb < 0.0 {
return Err(format!("invalid allocated_mb: {allocated_mb}"));
}
if !reserved_mb.is_finite() || reserved_mb < 0.0 {
return Err(format!("invalid reserved_mb: {reserved_mb}"));
}
Ok(Self {
peak_memory_mb,
allocated_mb,
reserved_mb,
})
}
}
/// Throughput statistics.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ThroughputMetrics {
/// Samples processed per second
pub samples_per_sec: f64,
/// Tokens processed per second (optional, for sequence models)
pub tokens_per_sec: Option<f64>,
}
impl ThroughputMetrics {
/// Creates new throughput metrics.
///
/// # Errors
/// Returns an error if `samples_per_sec` is invalid.
pub fn new(samples_per_sec: f64, tokens_per_sec: Option<f64>) -> Result<Self, String> {
if !samples_per_sec.is_finite() || samples_per_sec < 0.0 {
return Err(format!("invalid samples_per_sec: {samples_per_sec}"));
}
if let Some(tps) = tokens_per_sec
&& (!tps.is_finite() || tps < 0.0)
{
return Err(format!("invalid tokens_per_sec: {tps}"));
}
Ok(Self {
samples_per_sec,
tokens_per_sec,
})
}
/// Calculates throughput from batch size and mean latency.
///
/// # Arguments
/// * `batch_size` - Number of samples in batch
/// * `mean_latency_ms` - Mean latency in milliseconds
///
/// # Errors
/// Returns an error if inputs are invalid.
pub fn from_latency(batch_size: usize, mean_latency_ms: f64) -> Result<Self, String> {
if batch_size == 0 {
return Err("batch_size cannot be zero".to_string());
}
if !mean_latency_ms.is_finite() || mean_latency_ms <= 0.0 {
return Err(format!("invalid mean_latency_ms: {mean_latency_ms}"));
}
let samples_per_sec = (batch_size as f64 * 1000.0) / mean_latency_ms;
Self::new(samples_per_sec, None)
}
}
/// Computes the percentile of a sorted array.
fn percentile(sorted_data: &[f64], p: f64) -> f64 {
if sorted_data.is_empty() {
return 0.0;
}
if sorted_data.len() == 1 {
return sorted_data[0];
}
let rank = (p / 100.0) * (sorted_data.len() - 1) as f64;
let lower_idx = rank.floor() as usize;
let upper_idx = rank.ceil() as usize;
let weight = rank - lower_idx as f64;
sorted_data[lower_idx] * (1.0 - weight) + sorted_data[upper_idx] * weight
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_latency_metrics_from_measurements_success() {
let measurements = vec![10.0, 20.0, 15.0, 25.0, 30.0];
let metrics = LatencyMetrics::from_measurements(&measurements);
assert!(metrics.is_ok());
let m = metrics.unwrap();
assert_relative_eq!(m.mean_ms, 20.0, epsilon = 0.01);
assert_relative_eq!(m.min_ms, 10.0, epsilon = 0.01);
assert_relative_eq!(m.max_ms, 30.0, epsilon = 0.01);
}
#[test]
fn test_latency_metrics_empty_measurements() {
let measurements: Vec<f64> = vec![];
let result = LatencyMetrics::from_measurements(&measurements);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "measurements cannot be empty");
}
#[test]
fn test_latency_metrics_invalid_measurement_negative() {
let measurements = vec![10.0, -5.0, 15.0];
let result = LatencyMetrics::from_measurements(&measurements);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid measurement"));
}
#[test]
fn test_latency_metrics_invalid_measurement_nan() {
let measurements = vec![10.0, f64::NAN, 15.0];
let result = LatencyMetrics::from_measurements(&measurements);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid measurement"));
}
#[test]
fn test_latency_metrics_invalid_measurement_infinity() {
let measurements = vec![10.0, f64::INFINITY, 15.0];
let result = LatencyMetrics::from_measurements(&measurements);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid measurement"));
}
#[test]
fn test_latency_metrics_percentile_calculations() {
let measurements: Vec<f64> = (1..=100).map(|x| x as f64).collect();
let metrics = LatencyMetrics::from_measurements(&measurements).unwrap();
assert_relative_eq!(metrics.p50_ms, 50.0, epsilon = 1.0);
assert_relative_eq!(metrics.p95_ms, 95.0, epsilon = 1.0);
assert_relative_eq!(metrics.p99_ms, 99.0, epsilon = 1.0);
}
#[test]
fn test_latency_metrics_single_measurement() {
let measurements = vec![42.5];
let metrics = LatencyMetrics::from_measurements(&measurements).unwrap();
assert_relative_eq!(metrics.mean_ms, 42.5, epsilon = 0.01);
assert_relative_eq!(metrics.std_ms, 0.0, epsilon = 0.01);
assert_relative_eq!(metrics.min_ms, 42.5, epsilon = 0.01);
assert_relative_eq!(metrics.max_ms, 42.5, epsilon = 0.01);
assert_relative_eq!(metrics.p50_ms, 42.5, epsilon = 0.01);
assert_relative_eq!(metrics.p95_ms, 42.5, epsilon = 0.01);
assert_relative_eq!(metrics.p99_ms, 42.5, epsilon = 0.01);
}
#[test]
fn test_latency_metrics_std_calculation() {
let measurements = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let metrics = LatencyMetrics::from_measurements(&measurements).unwrap();
assert_relative_eq!(metrics.mean_ms, 5.0, epsilon = 0.01);
assert_relative_eq!(metrics.std_ms, 2.0, epsilon = 0.01);
}
#[test]
fn test_latency_metrics_serialization() {
let measurements = vec![10.0, 20.0, 30.0];
let metrics = LatencyMetrics::from_measurements(&measurements).unwrap();
let json = serde_json::to_string(&metrics).expect("serialization failed");
assert!(json.contains("\"mean_ms\""));
assert!(json.contains("\"std_ms\""));
}
#[test]
fn test_memory_metrics_new_success() {
let metrics = MemoryMetrics::new(100.0, 80.0, 120.0);
assert!(metrics.is_ok());
let m = metrics.unwrap();
assert_relative_eq!(m.peak_memory_mb, 100.0, epsilon = 0.01);
assert_relative_eq!(m.allocated_mb, 80.0, epsilon = 0.01);
assert_relative_eq!(m.reserved_mb, 120.0, epsilon = 0.01);
}
#[test]
fn test_memory_metrics_negative_peak() {
let result = MemoryMetrics::new(-100.0, 80.0, 120.0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid peak_memory_mb"));
}
#[test]
fn test_memory_metrics_negative_allocated() {
let result = MemoryMetrics::new(100.0, -80.0, 120.0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid allocated_mb"));
}
#[test]
fn test_memory_metrics_negative_reserved() {
let result = MemoryMetrics::new(100.0, 80.0, -120.0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid reserved_mb"));
}
#[test]
fn test_memory_metrics_nan_values() {
assert!(MemoryMetrics::new(f64::NAN, 80.0, 120.0).is_err());
assert!(MemoryMetrics::new(100.0, f64::NAN, 120.0).is_err());
assert!(MemoryMetrics::new(100.0, 80.0, f64::NAN).is_err());
}
#[test]
fn test_memory_metrics_infinity_values() {
assert!(MemoryMetrics::new(f64::INFINITY, 80.0, 120.0).is_err());
assert!(MemoryMetrics::new(100.0, f64::INFINITY, 120.0).is_err());
assert!(MemoryMetrics::new(100.0, 80.0, f64::INFINITY).is_err());
}
#[test]
fn test_memory_metrics_zero_values() {
let metrics = MemoryMetrics::new(0.0, 0.0, 0.0);
assert!(metrics.is_ok());
}
#[test]
fn test_memory_metrics_serialization() {
let metrics = MemoryMetrics::new(100.0, 80.0, 120.0).unwrap();
let json = serde_json::to_string(&metrics).expect("serialization failed");
assert!(json.contains("\"peak_memory_mb\""));
assert!(json.contains("\"allocated_mb\""));
assert!(json.contains("\"reserved_mb\""));
}
#[test]
fn test_throughput_metrics_new_success() {
let metrics = ThroughputMetrics::new(100.0, Some(500.0));
assert!(metrics.is_ok());
let m = metrics.unwrap();
assert_relative_eq!(m.samples_per_sec, 100.0, epsilon = 0.01);
assert!(m.tokens_per_sec.is_some());
assert_relative_eq!(m.tokens_per_sec.unwrap(), 500.0, epsilon = 0.01);
}
#[test]
fn test_throughput_metrics_new_without_tokens() {
let metrics = ThroughputMetrics::new(100.0, None);
assert!(metrics.is_ok());
let m = metrics.unwrap();
assert_relative_eq!(m.samples_per_sec, 100.0, epsilon = 0.01);
assert!(m.tokens_per_sec.is_none());
}
#[test]
fn test_throughput_metrics_negative_samples() {
let result = ThroughputMetrics::new(-100.0, None);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid samples_per_sec"));
}
#[test]
fn test_throughput_metrics_negative_tokens() {
let result = ThroughputMetrics::new(100.0, Some(-500.0));
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid tokens_per_sec"));
}
#[test]
fn test_throughput_metrics_nan_samples() {
let result = ThroughputMetrics::new(f64::NAN, None);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid samples_per_sec"));
}
#[test]
fn test_throughput_metrics_infinity_tokens() {
let result = ThroughputMetrics::new(100.0, Some(f64::INFINITY));
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid tokens_per_sec"));
}
#[test]
fn test_throughput_from_latency_success() {
let metrics = ThroughputMetrics::from_latency(8, 100.0);
assert!(metrics.is_ok());
let m = metrics.unwrap();
assert_relative_eq!(m.samples_per_sec, 80.0, epsilon = 0.01);
}
#[test]
fn test_throughput_from_latency_zero_batch_size() {
let result = ThroughputMetrics::from_latency(0, 100.0);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "batch_size cannot be zero");
}
#[test]
fn test_throughput_from_latency_zero_latency() {
let result = ThroughputMetrics::from_latency(8, 0.0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid mean_latency_ms"));
}
#[test]
fn test_throughput_from_latency_negative_latency() {
let result = ThroughputMetrics::from_latency(8, -100.0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid mean_latency_ms"));
}
#[test]
fn test_throughput_from_latency_calculation() {
let metrics = ThroughputMetrics::from_latency(32, 50.0).unwrap();
assert_relative_eq!(metrics.samples_per_sec, 640.0, epsilon = 0.1);
}
#[test]
fn test_throughput_metrics_serialization() {
let metrics = ThroughputMetrics::new(100.0, Some(500.0)).unwrap();
let json = serde_json::to_string(&metrics).expect("serialization failed");
assert!(json.contains("\"samples_per_sec\""));
assert!(json.contains("\"tokens_per_sec\""));
}
#[test]
fn test_percentile_empty_array() {
let data: Vec<f64> = vec![];
let p50 = percentile(&data, 50.0);
assert_relative_eq!(p50, 0.0, epsilon = 0.01);
}
#[test]
fn test_percentile_single_element() {
let data = vec![42.0];
let p50 = percentile(&data, 50.0);
assert_relative_eq!(p50, 42.0, epsilon = 0.01);
}
#[test]
fn test_percentile_interpolation() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let p50 = percentile(&data, 50.0);
assert_relative_eq!(p50, 3.0, epsilon = 0.01);
let p75 = percentile(&data, 75.0);
assert_relative_eq!(p75, 4.0, epsilon = 0.01);
}
#[test]
fn test_percentile_edge_cases() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let p0 = percentile(&data, 0.0);
assert_relative_eq!(p0, 1.0, epsilon = 0.01);
let p100 = percentile(&data, 100.0);
assert_relative_eq!(p100, 5.0, epsilon = 0.01);
}
}