1use alloc::format;
2use alloc::string::String;
3use alloc::vec;
4use alloc::vec::Vec;
5use core::fmt::Display;
6use core::time::Duration;
7
8pub use crate::profile::{Instant, TimingMethod};
9
10use crate::work::Work;
11
12#[cfg(feature = "std")]
13pub use crate::profile::ProfileDuration;
14
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[derive(new, Debug, Clone)]
18pub struct BenchmarkDurations {
19 pub timing_method: TimingMethod,
21 pub durations: Vec<Duration>,
23}
24
25impl BenchmarkDurations {
26 pub fn from_durations(timing_method: TimingMethod, durations: Vec<Duration>) -> Self {
28 Self {
29 timing_method,
30 durations,
31 }
32 }
33
34 fn min_max_median_durations(&self) -> (Duration, Duration, Duration) {
36 let mut sorted = self.durations.clone();
37 sorted.sort();
38 let min = *sorted.first().unwrap();
39 let max = *sorted.last().unwrap();
40 let median = *sorted.get(sorted.len() / 2).unwrap();
41 (min, max, median)
42 }
43
44 pub(crate) fn mean_duration(&self) -> Duration {
46 self.durations.iter().sum::<Duration>() / self.durations.len() as u32
47 }
48
49 pub(crate) fn variance_duration(&self, mean: Duration) -> Duration {
51 self.durations
52 .iter()
53 .map(|duration| {
54 let tmp = duration.as_secs_f64() - mean.as_secs_f64();
55 Duration::from_secs_f64(tmp * tmp)
56 })
57 .sum::<Duration>()
58 / self.durations.len() as u32
59 }
60}
61
62impl Display for BenchmarkDurations {
63 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64 let computed = BenchmarkComputations::new(self);
65 let BenchmarkComputations {
66 mean,
67 median,
68 variance,
69 min,
70 max,
71 } = computed;
72 let num_sample = self.durations.len();
73 let timing_method = self.timing_method;
74
75 f.write_str(
76 format!(
77 "
78―――――――― Result ―――――――――
79 Timing {timing_method}
80 Samples {num_sample}
81 Mean {mean:.3?}
82 Variance {variance:.3?}
83 Median {median:.3?}
84 Min {min:.3?}
85 Max {max:.3?}
86―――――――――――――――――――――――――"
87 )
88 .as_str(),
89 )
90 }
91}
92
93#[cfg_attr(
95 feature = "serde",
96 derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)
97)]
98#[derive(Debug, Default, Clone)]
99pub struct BenchmarkComputations {
100 pub mean: Duration,
102 pub median: Duration,
104 pub variance: Duration,
106 pub min: Duration,
108 pub max: Duration,
110}
111
112impl BenchmarkComputations {
113 pub fn new(durations: &BenchmarkDurations) -> Self {
115 let mean = durations.mean_duration();
116 let (min, max, median) = durations.min_max_median_durations();
117 Self {
118 mean,
119 median,
120 min,
121 max,
122 variance: durations.variance_duration(mean),
123 }
124 }
125
126 pub fn score(&self) -> u64 {
128 const ALPHA: f64 = 0.8;
132
133 let min_ns = self.min.as_nanos() as f64;
134 let median_ns = self.median.as_nanos() as f64;
135 let variance_ns = self.variance.as_nanos() as f64;
136 let mean_ns = self.mean.as_nanos() as f64;
137
138 let base_score = (min_ns * ALPHA) + (median_ns * (1.0 - ALPHA));
140
141 let std_dev = num_traits::Float::sqrt(variance_ns);
144
145 let coefficient_of_variation = 1.0
147 + (std_dev
148 / (
149 1.0 + mean_ns
152 ));
153
154 (base_score * coefficient_of_variation) as u64
156 }
157}
158
159pub trait Benchmark {
161 type Input: Clone;
163 type Output;
165
166 fn prepare(&self) -> Self::Input;
174
175 fn execute(&self, input: Self::Input) -> Result<Self::Output, String>;
180
181 fn num_samples(&self) -> usize {
183 const DEFAULT: usize = 15;
184 #[cfg(feature = "std")]
185 {
186 std::env::var("BENCH_NUM_SAMPLES")
187 .map(|val| str::parse::<usize>(&val).unwrap_or(DEFAULT))
188 .unwrap_or(DEFAULT)
189 }
190
191 #[cfg(not(feature = "std"))]
192 {
193 DEFAULT
194 }
195 }
196
197 fn name(&self) -> String;
200
201 fn options(&self) -> Option<String> {
203 None
204 }
205
206 fn shapes(&self) -> Vec<Vec<usize>> {
208 vec![]
209 }
210
211 fn work(&self) -> Option<Work> {
219 None
220 }
221
222 fn sync(&self);
224
225 #[cfg(feature = "std")]
227 fn profile(&self, args: Self::Input) -> Result<ProfileDuration, String> {
228 self.profile_full(args)
229 }
230
231 #[cfg(feature = "std")]
234 fn profile_full(&self, args: Self::Input) -> Result<ProfileDuration, String> {
235 self.sync();
236 let start_time = Instant::now();
237 let out = self.execute(args)?;
238 self.sync();
239 core::mem::drop(out);
240 Ok(ProfileDuration::new_system_time(start_time, Instant::now()))
241 }
242
243 #[allow(unused_variables)]
245 fn run(&self, timing_method: TimingMethod) -> Result<BenchmarkDurations, String> {
246 #[cfg(not(feature = "std"))]
247 panic!("Attempting to run benchmark in a no-std environment");
248
249 #[cfg(feature = "std")]
250 {
251 let execute = |args: &Self::Input| {
252 let profile: Result<ProfileDuration, String> = match timing_method {
253 TimingMethod::System => self.profile_full(args.clone()),
254 TimingMethod::Device => self.profile(args.clone()),
255 };
256 let profile = match profile {
257 Ok(val) => val,
258 Err(err) => return Err(err),
259 };
260 Ok(cubecl_environment::future::block_on(profile.resolve()))
261 };
262 let args = self.prepare();
263
264 for _ in 0..5 {
269 let _duration: Result<crate::profile::ProfileTicks, _> = execute(&args);
270 }
271
272 let mut durations = Vec::with_capacity(self.num_samples());
274 for _ in 0..self.num_samples() {
275 match execute(&args) {
276 Ok(val) => durations.push(val.duration()),
277 Err(err) => {
278 return Err(err);
279 }
280 }
281 }
282
283 Ok(BenchmarkDurations {
284 timing_method,
285 durations,
286 })
287 }
288 }
289}
290
291#[derive(Clone)]
293pub struct BenchmarkResult {
294 pub raw: BenchmarkDurations,
296 pub computed: BenchmarkComputations,
298 pub git_hash: String,
300 pub name: String,
302 pub options: Option<String>,
304 pub shapes: Vec<Vec<usize>>,
306 pub timestamp: u128,
308}
309
310impl Display for BenchmarkResult {
311 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312 f.write_str(
313 format!(
314 "
315 Timestamp: {}
316 Git Hash: {}
317 Benchmarking - {}{}
318 ",
319 self.timestamp, self.git_hash, self.name, self.raw
320 )
321 .as_str(),
322 )
323 }
324}
325
326#[cfg(feature = "std")]
327pub fn run_benchmark<BM>(benchmark: BM) -> Result<BenchmarkResult, String>
329where
330 BM: Benchmark,
331{
332 use std::string::ToString;
333
334 let timestamp = std::time::SystemTime::now()
335 .duration_since(std::time::UNIX_EPOCH)
336 .unwrap()
337 .as_millis();
338 let output = std::process::Command::new("git")
339 .args(["rev-parse", "HEAD"])
340 .output()
341 .unwrap();
342 let git_hash = String::from_utf8(output.stdout).unwrap().trim().to_string();
343 let durations = benchmark.run(TimingMethod::System)?;
344
345 Ok(BenchmarkResult {
346 raw: durations.clone(),
347 computed: BenchmarkComputations::new(&durations),
348 git_hash,
349 name: benchmark.name(),
350 options: benchmark.options(),
351 shapes: benchmark.shapes(),
352 timestamp,
353 })
354}
355
356#[cfg(test)]
357#[cfg(feature = "std")]
358mod tests {
359 use super::*;
360 use alloc::vec;
361
362 #[test_log::test]
363 fn test_min_max_median_durations_even_number_of_samples() {
364 let durations = BenchmarkDurations {
365 timing_method: TimingMethod::System,
366 durations: vec![
367 Duration::new(10, 0),
368 Duration::new(20, 0),
369 Duration::new(30, 0),
370 Duration::new(40, 0),
371 Duration::new(50, 0),
372 ],
373 };
374 let (min, max, median) = durations.min_max_median_durations();
375 assert_eq!(min, Duration::from_secs(10));
376 assert_eq!(max, Duration::from_secs(50));
377 assert_eq!(median, Duration::from_secs(30));
378 }
379
380 #[test_log::test]
381 fn test_min_max_median_durations_odd_number_of_samples() {
382 let durations = BenchmarkDurations {
383 timing_method: TimingMethod::System,
384 durations: vec![
385 Duration::new(18, 5),
386 Duration::new(20, 0),
387 Duration::new(30, 0),
388 Duration::new(40, 0),
389 ],
390 };
391 let (min, max, median) = durations.min_max_median_durations();
392 assert_eq!(min, Duration::from_nanos(18000000005_u64));
393 assert_eq!(max, Duration::from_secs(40));
394 assert_eq!(median, Duration::from_secs(30));
395 }
396
397 #[test_log::test]
398 fn test_mean_duration() {
399 let durations = BenchmarkDurations {
400 timing_method: TimingMethod::System,
401 durations: vec![
402 Duration::new(10, 0),
403 Duration::new(20, 0),
404 Duration::new(30, 0),
405 Duration::new(40, 0),
406 ],
407 };
408 let mean = durations.mean_duration();
409 assert_eq!(mean, Duration::from_secs(25));
410 }
411
412 #[test_log::test]
413 fn test_variance_duration() {
414 let durations = BenchmarkDurations {
415 timing_method: TimingMethod::System,
416 durations: vec![
417 Duration::new(10, 0),
418 Duration::new(20, 0),
419 Duration::new(30, 0),
420 Duration::new(40, 0),
421 Duration::new(50, 0),
422 ],
423 };
424 let mean = durations.mean_duration();
425 let variance = durations.variance_duration(mean);
426 assert_eq!(variance, Duration::from_secs(200));
427 }
428}