use super::{LeaderboardCategory, LeaderboardEntry};
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RankingCriteria {
pub primary_metric: RankingMetric,
pub secondary_metric: Option<RankingMetric>,
pub order: SortOrder,
}
impl Default for RankingCriteria {
fn default() -> Self {
Self {
primary_metric: RankingMetric::Latency,
secondary_metric: Some(RankingMetric::Throughput),
order: SortOrder::Ascending,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RankingMetric {
Latency,
Throughput,
TokensPerSecond,
Memory,
PeakMemory,
GPUUtilization,
Accuracy,
Energy,
Date,
Custom(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum SortOrder {
Ascending,
Descending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaderboardRanking {
pub category: LeaderboardCategory,
pub criteria: RankingCriteria,
pub entries: Vec<LeaderboardEntry>,
pub generated_at: DateTime<Utc>,
}
pub trait RankingAlgorithm: Send + Sync {
fn rank(
&self,
entries: Vec<LeaderboardEntry>,
criteria: &RankingCriteria,
) -> Result<Vec<LeaderboardEntry>>;
}
pub struct DefaultRankingAlgorithm;
impl RankingAlgorithm for DefaultRankingAlgorithm {
fn rank(
&self,
mut entries: Vec<LeaderboardEntry>,
criteria: &RankingCriteria,
) -> Result<Vec<LeaderboardEntry>> {
entries.sort_by(|a, b| criteria.compare(a, b));
Ok(entries)
}
}
impl RankingCriteria {
pub fn compare(&self, a: &LeaderboardEntry, b: &LeaderboardEntry) -> Ordering {
let primary_cmp = self.compare_by_metric(a, b, &self.primary_metric);
if primary_cmp != Ordering::Equal {
return if self.order == SortOrder::Ascending {
primary_cmp
} else {
primary_cmp.reverse()
};
}
if let Some(secondary) = &self.secondary_metric {
let secondary_cmp = self.compare_by_metric(a, b, secondary);
if self.order == SortOrder::Ascending {
secondary_cmp
} else {
secondary_cmp.reverse()
}
} else {
b.timestamp.cmp(&a.timestamp)
}
}
fn compare_by_metric(
&self,
a: &LeaderboardEntry,
b: &LeaderboardEntry,
metric: &RankingMetric,
) -> Ordering {
match metric {
RankingMetric::Latency => a
.metrics
.latency_ms
.partial_cmp(&b.metrics.latency_ms)
.unwrap_or(Ordering::Equal),
RankingMetric::Throughput => {
let a_val = a.metrics.throughput.unwrap_or(0.0);
let b_val = b.metrics.throughput.unwrap_or(0.0);
b_val.partial_cmp(&a_val).unwrap_or(Ordering::Equal) },
RankingMetric::TokensPerSecond => {
let a_val = a.metrics.tokens_per_second.unwrap_or(0.0);
let b_val = b.metrics.tokens_per_second.unwrap_or(0.0);
b_val.partial_cmp(&a_val).unwrap_or(Ordering::Equal) },
RankingMetric::Memory => {
let a_val = a.metrics.memory_mb.unwrap_or(f64::MAX);
let b_val = b.metrics.memory_mb.unwrap_or(f64::MAX);
a_val.partial_cmp(&b_val).unwrap_or(Ordering::Equal)
},
RankingMetric::PeakMemory => {
let a_val = a.metrics.peak_memory_mb.unwrap_or(f64::MAX);
let b_val = b.metrics.peak_memory_mb.unwrap_or(f64::MAX);
a_val.partial_cmp(&b_val).unwrap_or(Ordering::Equal)
},
RankingMetric::GPUUtilization => {
let a_val = a.metrics.gpu_utilization.unwrap_or(0.0);
let b_val = b.metrics.gpu_utilization.unwrap_or(0.0);
b_val.partial_cmp(&a_val).unwrap_or(Ordering::Equal) },
RankingMetric::Accuracy => {
let a_val = a.metrics.accuracy.unwrap_or(0.0);
let b_val = b.metrics.accuracy.unwrap_or(0.0);
b_val.partial_cmp(&a_val).unwrap_or(Ordering::Equal) },
RankingMetric::Energy => {
let a_val = a.metrics.energy_watts.unwrap_or(f64::MAX);
let b_val = b.metrics.energy_watts.unwrap_or(f64::MAX);
a_val.partial_cmp(&b_val).unwrap_or(Ordering::Equal)
},
RankingMetric::Date => {
b.timestamp.cmp(&a.timestamp) },
RankingMetric::Custom(name) => {
let a_val = a.metrics.custom_metrics.get(name).copied().unwrap_or(0.0);
let b_val = b.metrics.custom_metrics.get(name).copied().unwrap_or(0.0);
a_val.partial_cmp(&b_val).unwrap_or(Ordering::Equal)
},
}
}
}
pub struct WeightedRankingAlgorithm {
weights: Vec<(RankingMetric, f64)>,
}
impl WeightedRankingAlgorithm {
pub fn new(weights: Vec<(RankingMetric, f64)>) -> Self {
Self { weights }
}
fn calculate_score(&self, entry: &LeaderboardEntry) -> f64 {
let mut score = 0.0;
for (metric, weight) in &self.weights {
let value = match metric {
RankingMetric::Latency => {
1.0 / (1.0 + entry.metrics.latency_ms)
},
RankingMetric::Throughput => {
entry.metrics.throughput.unwrap_or(0.0) / 1000.0
},
RankingMetric::TokensPerSecond => {
entry.metrics.tokens_per_second.unwrap_or(0.0) / 10000.0
},
RankingMetric::Memory => {
1.0 / (1.0 + entry.metrics.memory_mb.unwrap_or(1000.0))
},
RankingMetric::PeakMemory => {
1.0 / (1.0 + entry.metrics.peak_memory_mb.unwrap_or(1000.0))
},
RankingMetric::GPUUtilization => {
entry.metrics.gpu_utilization.unwrap_or(0.0) / 100.0
},
RankingMetric::Accuracy => {
entry.metrics.accuracy.unwrap_or(0.0)
},
RankingMetric::Energy => {
1.0 / (1.0 + entry.metrics.energy_watts.unwrap_or(100.0))
},
RankingMetric::Date => {
entry.timestamp.timestamp() as f64 / 86400.0 / 20000.0
},
RankingMetric::Custom(name) => {
entry.metrics.custom_metrics.get(name).copied().unwrap_or(0.0)
},
};
score += value * weight;
}
score
}
}
impl RankingAlgorithm for WeightedRankingAlgorithm {
fn rank(
&self,
entries: Vec<LeaderboardEntry>,
_criteria: &RankingCriteria,
) -> Result<Vec<LeaderboardEntry>> {
let mut scored_entries: Vec<(f64, LeaderboardEntry)> = entries
.into_iter()
.map(|entry| {
let score = self.calculate_score(&entry);
(score, entry)
})
.collect();
scored_entries.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
let ranked = scored_entries.into_iter().map(|(_, entry)| entry).collect();
Ok(ranked)
}
}
pub struct EloRankingAlgorithm {
k_factor: f64,
}
impl EloRankingAlgorithm {
pub fn new(k_factor: f64) -> Self {
Self { k_factor }
}
pub fn calculate_rating_change(&self, winner_rating: f64, loser_rating: f64) -> (f64, f64) {
let expected_winner = 1.0 / (1.0 + 10_f64.powf((loser_rating - winner_rating) / 400.0));
let expected_loser = 1.0 - expected_winner;
let winner_change = self.k_factor * (1.0 - expected_winner);
let loser_change = self.k_factor * (0.0 - expected_loser);
(winner_change, loser_change)
}
}
pub struct ParetoRankingAlgorithm;
impl ParetoRankingAlgorithm {
fn dominates(a: &LeaderboardEntry, b: &LeaderboardEntry) -> bool {
let mut better_in_at_least_one = false;
let metrics = [
(a.metrics.latency_ms, b.metrics.latency_ms, true), (
a.metrics.throughput.unwrap_or(0.0),
b.metrics.throughput.unwrap_or(0.0),
false,
), (
a.metrics.memory_mb.unwrap_or(f64::MAX),
b.metrics.memory_mb.unwrap_or(f64::MAX),
true,
), (
a.metrics.accuracy.unwrap_or(0.0),
b.metrics.accuracy.unwrap_or(0.0),
false,
), ];
for (a_val, b_val, lower_is_better) in metrics {
let comparison = if lower_is_better { a_val <= b_val } else { a_val >= b_val };
if !comparison {
return false; }
if (lower_is_better && a_val < b_val) || (!lower_is_better && a_val > b_val) {
better_in_at_least_one = true;
}
}
better_in_at_least_one
}
}
impl RankingAlgorithm for ParetoRankingAlgorithm {
fn rank(
&self,
entries: Vec<LeaderboardEntry>,
_criteria: &RankingCriteria,
) -> Result<Vec<LeaderboardEntry>> {
let mut ranked = Vec::new();
let mut remaining = entries;
while !remaining.is_empty() {
let mut frontier = Vec::new();
let mut dominated = Vec::new();
for entry in remaining {
let mut is_dominated = false;
for other in &frontier {
if Self::dominates(other, &entry) {
is_dominated = true;
break;
}
}
if !is_dominated {
frontier.retain(|other| !Self::dominates(&entry, other));
frontier.push(entry);
} else {
dominated.push(entry);
}
}
frontier.sort_by(|a, b| {
a.metrics
.latency_ms
.partial_cmp(&b.metrics.latency_ms)
.unwrap_or(Ordering::Equal)
});
ranked.extend(frontier);
remaining = dominated;
}
Ok(ranked)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::leaderboard::*;
use std::collections::HashMap;
fn create_test_entry(latency: f64, throughput: f64) -> LeaderboardEntry {
LeaderboardEntry {
id: uuid::Uuid::new_v4(),
timestamp: chrono::Utc::now(),
model_name: "test".to_string(),
model_version: "1.0".to_string(),
benchmark_name: "test".to_string(),
category: LeaderboardCategory::Inference,
hardware: HardwareInfo {
cpu: "Test".to_string(),
cpu_cores: 8,
gpu: None,
gpu_count: None,
memory_gb: 16.0,
accelerator: None,
platform: "test".to_string(),
},
software: SoftwareInfo {
framework_version: "0.1.0".to_string(),
rust_version: "1.75".to_string(),
os: "Test".to_string(),
optimization_level: OptimizationLevel::O2,
precision: Precision::FP32,
quantization: None,
compiler_flags: vec![],
},
metrics: PerformanceMetrics {
latency_ms: latency,
latency_percentiles: LatencyPercentiles {
p50: latency * 0.9,
p90: latency * 1.1,
p95: latency * 1.2,
p99: latency * 1.5,
p999: latency * 2.0,
},
throughput: Some(throughput),
tokens_per_second: None,
memory_mb: None,
peak_memory_mb: None,
gpu_utilization: None,
accuracy: None,
energy_watts: None,
custom_metrics: HashMap::new(),
},
metadata: HashMap::new(),
validated: true,
submitter: SubmitterInfo {
name: "Test".to_string(),
organization: None,
email: None,
github: None,
},
tags: vec![],
}
}
#[test]
fn test_ranking_criteria_compare() {
let criteria = RankingCriteria {
primary_metric: RankingMetric::Latency,
secondary_metric: Some(RankingMetric::Throughput),
order: SortOrder::Ascending,
};
let entry1 = create_test_entry(10.0, 100.0);
let entry2 = create_test_entry(20.0, 50.0);
assert_eq!(criteria.compare(&entry1, &entry2), Ordering::Less);
}
#[test]
fn test_default_ranking_algorithm() {
let algo = DefaultRankingAlgorithm;
let criteria = RankingCriteria::default();
let entries = vec![
create_test_entry(20.0, 50.0),
create_test_entry(10.0, 100.0),
create_test_entry(30.0, 33.3),
];
let ranked = algo.rank(entries, &criteria).expect("operation failed in test");
assert_eq!(ranked[0].metrics.latency_ms, 10.0);
assert_eq!(ranked[1].metrics.latency_ms, 20.0);
assert_eq!(ranked[2].metrics.latency_ms, 30.0);
}
#[test]
fn test_weighted_ranking() {
let algo = WeightedRankingAlgorithm::new(vec![
(RankingMetric::Latency, 0.6),
(RankingMetric::Throughput, 0.4),
]);
let entries = vec![
create_test_entry(20.0, 200.0), create_test_entry(10.0, 50.0), create_test_entry(30.0, 150.0), ];
let ranked = algo
.rank(entries, &RankingCriteria::default())
.expect("operation failed in test");
assert!(ranked.len() == 3);
}
#[test]
fn test_elo_rating_calculation() {
let elo = EloRankingAlgorithm::new(32.0);
let (winner_change, loser_change) = elo.calculate_rating_change(1500.0, 1500.0);
assert!((winner_change - 16.0).abs() < 0.1);
assert!((loser_change + 16.0).abs() < 0.1);
}
}