entrenar/efficiency/benchmark/
entry.rs1use crate::efficiency::{ComputeDevice, CostMetrics, EnergyMetrics, ModelParadigm};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct BenchmarkEntry {
9 pub run_id: String,
11 pub paradigm: ModelParadigm,
13 pub device: ComputeDevice,
15 pub quality_score: f64,
17 pub cost: CostMetrics,
19 pub energy: EnergyMetrics,
21}
22
23impl BenchmarkEntry {
24 pub fn new(
26 run_id: impl Into<String>,
27 paradigm: ModelParadigm,
28 device: ComputeDevice,
29 quality_score: f64,
30 cost: CostMetrics,
31 energy: EnergyMetrics,
32 ) -> Self {
33 Self { run_id: run_id.into(), paradigm, device, quality_score, cost, energy }
34 }
35
36 pub fn efficiency_score(&self) -> f64 {
38 if self.cost.total_cost_usd > 0.0 {
39 self.quality_score / self.cost.total_cost_usd
40 } else {
41 f64::INFINITY
42 }
43 }
44
45 pub fn energy_efficiency(&self) -> f64 {
47 let kwh = self.energy.kwh();
48 if kwh > 0.0 {
49 self.quality_score / kwh
50 } else {
51 f64::INFINITY
52 }
53 }
54
55 pub fn carbon_efficiency(&self) -> f64 {
57 if self.energy.carbon_kg > 0.0 {
58 self.quality_score / self.energy.carbon_kg
59 } else {
60 f64::INFINITY
61 }
62 }
63
64 pub fn dominates(&self, other: &Self) -> bool {
66 self.quality_score >= other.quality_score
67 && self.cost.total_cost_usd <= other.cost.total_cost_usd
68 && self.energy.joules_total <= other.energy.joules_total
69 && (self.quality_score > other.quality_score
70 || self.cost.total_cost_usd < other.cost.total_cost_usd
71 || self.energy.joules_total < other.energy.joules_total)
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::efficiency::device::CpuInfo;
79
80 fn make_entry(quality: f64, cost_usd: f64, joules: f64, carbon: f64) -> BenchmarkEntry {
81 let mut energy = EnergyMetrics::new(100.0, joules, 1000);
82 energy.carbon_kg = carbon;
83 BenchmarkEntry::new(
84 "test-run",
85 ModelParadigm::DeepLearning,
86 ComputeDevice::Cpu(CpuInfo::detect()),
87 quality,
88 CostMetrics {
89 total_cost_usd: cost_usd,
90 cost_per_sample_usd: cost_usd / 1000.0,
91 cost_per_epoch_usd: cost_usd / 10.0,
92 device_hours: 1.0,
93 rate_per_hour_usd: cost_usd,
94 },
95 energy,
96 )
97 }
98
99 #[test]
100 fn test_benchmark_entry_new() {
101 let entry = make_entry(0.95, 10.0, 36000.0, 0.5);
102 assert_eq!(entry.run_id, "test-run");
103 assert_eq!(entry.paradigm, ModelParadigm::DeepLearning);
104 assert!((entry.quality_score - 0.95).abs() < 1e-9);
105 assert!((entry.cost.total_cost_usd - 10.0).abs() < 1e-9);
106 }
107
108 #[test]
109 fn test_efficiency_score_normal() {
110 let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
111 assert!((entry.efficiency_score() - 0.09).abs() < 1e-9);
112 }
113
114 #[test]
115 fn test_efficiency_score_zero_cost() {
116 let entry = make_entry(0.9, 0.0, 36000.0, 0.5);
117 assert!(entry.efficiency_score().is_infinite());
118 }
119
120 #[test]
121 fn test_energy_efficiency_normal() {
122 let entry = make_entry(0.9, 10.0, 3600000.0, 0.5);
123 assert!((entry.energy_efficiency() - 0.9).abs() < 1e-9);
125 }
126
127 #[test]
128 fn test_energy_efficiency_zero_energy() {
129 let entry = make_entry(0.9, 10.0, 0.0, 0.5);
130 assert!(entry.energy_efficiency().is_infinite());
131 }
132
133 #[test]
134 fn test_carbon_efficiency_normal() {
135 let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
136 assert!((entry.carbon_efficiency() - 1.8).abs() < 1e-9);
138 }
139
140 #[test]
141 fn test_carbon_efficiency_zero_carbon() {
142 let entry = make_entry(0.9, 10.0, 36000.0, 0.0);
143 assert!(entry.carbon_efficiency().is_infinite());
144 }
145
146 #[test]
147 fn test_dominates_better_quality() {
148 let better = make_entry(0.95, 10.0, 36000.0, 0.5);
149 let worse = make_entry(0.90, 10.0, 36000.0, 0.5);
150 assert!(better.dominates(&worse));
151 assert!(!worse.dominates(&better));
152 }
153
154 #[test]
155 fn test_dominates_lower_cost() {
156 let better = make_entry(0.9, 5.0, 36000.0, 0.5);
157 let worse = make_entry(0.9, 10.0, 36000.0, 0.5);
158 assert!(better.dominates(&worse));
159 assert!(!worse.dominates(&better));
160 }
161
162 #[test]
163 fn test_dominates_lower_energy() {
164 let better = make_entry(0.9, 10.0, 18000.0, 0.5);
165 let worse = make_entry(0.9, 10.0, 36000.0, 0.5);
166 assert!(better.dominates(&worse));
167 assert!(!worse.dominates(&better));
168 }
169
170 #[test]
171 fn test_dominates_equal_entries() {
172 let entry1 = make_entry(0.9, 10.0, 36000.0, 0.5);
173 let entry2 = make_entry(0.9, 10.0, 36000.0, 0.5);
174 assert!(!entry1.dominates(&entry2));
176 assert!(!entry2.dominates(&entry1));
177 }
178
179 #[test]
180 fn test_dominates_tradeoff() {
181 let entry1 = make_entry(0.95, 20.0, 36000.0, 0.5);
183 let entry2 = make_entry(0.90, 10.0, 36000.0, 0.5);
184 assert!(!entry1.dominates(&entry2));
185 assert!(!entry2.dominates(&entry1));
186 }
187
188 #[test]
189 fn test_benchmark_entry_clone() {
190 let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
191 let cloned = entry.clone();
192 assert_eq!(entry, cloned);
193 }
194
195 #[test]
196 fn test_benchmark_entry_serde() {
197 let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
198 let json = serde_json::to_string(&entry).expect("JSON serialization should succeed");
199 let deserialized: BenchmarkEntry =
200 serde_json::from_str(&json).expect("JSON deserialization should succeed");
201 assert_eq!(entry.run_id, deserialized.run_id);
202 assert!((entry.quality_score - deserialized.quality_score).abs() < 1e-9);
203 }
204
205 #[test]
206 fn test_benchmark_entry_debug() {
207 let entry = make_entry(0.9, 10.0, 36000.0, 0.5);
208 let debug_str = format!("{entry:?}");
209 assert!(debug_str.contains("BenchmarkEntry"));
210 assert!(debug_str.contains("test-run"));
211 }
212}