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
159#[cfg(feature = "std")]
162const MIN_WARMUP_RUNS: usize = 5;
163
164pub trait Benchmark {
166 type Input: Clone;
168 type Output;
170
171 fn prepare(&self) -> Self::Input;
179
180 fn execute(&self, input: Self::Input) -> Result<Self::Output, String>;
185
186 fn warmup_budget(&self) -> Duration {
192 const DEFAULT_MS: u64 = 500;
193 #[cfg(feature = "std")]
194 {
195 Duration::from_millis(
196 std::env::var("BENCH_WARMUP_MS")
197 .map(|val| str::parse::<u64>(&val).unwrap_or(DEFAULT_MS))
198 .unwrap_or(DEFAULT_MS),
199 )
200 }
201
202 #[cfg(not(feature = "std"))]
203 {
204 Duration::from_millis(DEFAULT_MS)
205 }
206 }
207
208 fn num_samples(&self) -> usize {
210 const DEFAULT: usize = 15;
211 #[cfg(feature = "std")]
212 {
213 std::env::var("BENCH_NUM_SAMPLES")
214 .map(|val| str::parse::<usize>(&val).unwrap_or(DEFAULT))
215 .unwrap_or(DEFAULT)
216 }
217
218 #[cfg(not(feature = "std"))]
219 {
220 DEFAULT
221 }
222 }
223
224 fn name(&self) -> String;
227
228 fn options(&self) -> Option<String> {
230 None
231 }
232
233 fn shapes(&self) -> Vec<Vec<usize>> {
235 vec![]
236 }
237
238 fn work(&self) -> Option<Work> {
245 None
246 }
247
248 fn sync(&self);
250
251 #[cfg(feature = "std")]
253 fn profile(&self, args: Self::Input) -> Result<ProfileDuration, String> {
254 self.profile_full(args)
255 }
256
257 #[cfg(feature = "std")]
260 fn profile_full(&self, args: Self::Input) -> Result<ProfileDuration, String> {
261 self.sync();
262 let start_time = Instant::now();
263 let out = self.execute(args)?;
264 self.sync();
265 core::mem::drop(out);
266 Ok(ProfileDuration::new_system_time(start_time, Instant::now()))
267 }
268
269 #[allow(unused_variables)]
271 fn run(&self, timing_method: TimingMethod) -> Result<BenchmarkDurations, String> {
272 #[cfg(not(feature = "std"))]
273 panic!("Attempting to run benchmark in a no-std environment");
274
275 #[cfg(feature = "std")]
276 {
277 let execute = |args: &Self::Input| {
278 let profile: Result<ProfileDuration, String> = match timing_method {
279 TimingMethod::System => self.profile_full(args.clone()),
280 TimingMethod::Device => self.profile(args.clone()),
281 };
282 let profile = match profile {
283 Ok(val) => val,
284 Err(err) => return Err(err),
285 };
286 cubecl_environment::future::block_on(profile.resolve()).ok_or_else(|| {
290 alloc::string::String::from("the profiled window carried no measurement")
291 })
292 };
293 let args = self.prepare();
294
295 let budget = self.warmup_budget();
298 let warmup = Instant::now();
299 let (mut warmups, mut failures) = (0, 0);
300 while warmups < MIN_WARMUP_RUNS || warmup.elapsed() < budget {
301 let warmed: Result<crate::profile::ProfileTicks, String> = execute(&args);
302
303 match warmed {
304 Ok(_) => warmups += 1,
305 Err(_) => {
308 failures += 1;
309
310 if failures >= MIN_WARMUP_RUNS {
311 break;
312 }
313 }
314 }
315 }
316
317 let mut durations = Vec::with_capacity(self.num_samples());
319 for _ in 0..self.num_samples() {
320 match execute(&args) {
321 Ok(val) => durations.push(val.duration()),
322 Err(err) => {
323 return Err(err);
324 }
325 }
326 }
327
328 Ok(BenchmarkDurations {
329 timing_method,
330 durations,
331 })
332 }
333 }
334}
335
336#[derive(Clone)]
338pub struct BenchmarkResult {
339 pub raw: BenchmarkDurations,
341 pub computed: BenchmarkComputations,
343 pub git_hash: String,
345 pub name: String,
347 pub options: Option<String>,
349 pub shapes: Vec<Vec<usize>>,
351 pub timestamp: u128,
353}
354
355impl Display for BenchmarkResult {
356 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
357 f.write_str(
358 format!(
359 "
360 Timestamp: {}
361 Git Hash: {}
362 Benchmarking - {}{}
363 ",
364 self.timestamp, self.git_hash, self.name, self.raw
365 )
366 .as_str(),
367 )
368 }
369}
370
371#[cfg(feature = "std")]
372pub fn run_benchmark<BM>(benchmark: BM) -> Result<BenchmarkResult, String>
374where
375 BM: Benchmark,
376{
377 use std::string::ToString;
378
379 let timestamp = std::time::SystemTime::now()
380 .duration_since(std::time::UNIX_EPOCH)
381 .unwrap()
382 .as_millis();
383 let output = std::process::Command::new("git")
384 .args(["rev-parse", "HEAD"])
385 .output()
386 .unwrap();
387 let git_hash = String::from_utf8(output.stdout).unwrap().trim().to_string();
388 let durations = benchmark.run(TimingMethod::System)?;
389
390 Ok(BenchmarkResult {
391 raw: durations.clone(),
392 computed: BenchmarkComputations::new(&durations),
393 git_hash,
394 name: benchmark.name(),
395 options: benchmark.options(),
396 shapes: benchmark.shapes(),
397 timestamp,
398 })
399}
400
401#[cfg(test)]
402#[cfg(feature = "std")]
403mod tests {
404 use super::*;
405 use alloc::vec;
406 use core::cell::Cell;
407
408 struct TimedBench {
411 per_execution: Duration,
412 executions: Cell<usize>,
413 failures: Cell<usize>,
415 }
416
417 impl TimedBench {
418 fn new(per_execution: Duration) -> Self {
419 Self {
420 per_execution,
421 executions: Cell::new(0),
422 failures: Cell::new(0),
423 }
424 }
425
426 fn failing(self, launches: usize) -> Self {
427 self.failures.set(launches);
428
429 self
430 }
431 }
432
433 impl Benchmark for TimedBench {
434 type Input = ();
435 type Output = ();
436
437 fn prepare(&self) -> Self::Input {}
438
439 fn execute(&self, _input: Self::Input) -> Result<Self::Output, String> {
440 self.executions.set(self.executions.get() + 1);
441
442 if self.failures.get() > 0 {
443 self.failures.set(self.failures.get() - 1);
444
445 return Err(String::from("the launch failed"));
446 }
447
448 let start = Instant::now();
449 while start.elapsed() < self.per_execution {}
450
451 Ok(())
452 }
453
454 fn num_samples(&self) -> usize {
455 1
456 }
457
458 fn name(&self) -> String {
459 "timed".into()
460 }
461
462 fn sync(&self) {}
463 }
464
465 #[test_log::test]
469 fn a_kernel_the_launch_count_cannot_warm_is_held_for_the_budget() {
470 let bench = TimedBench::new(Duration::ZERO);
471 let start = Instant::now();
472
473 bench.run(TimingMethod::System).expect("the bench runs");
474
475 assert!(
476 start.elapsed() >= bench.warmup_budget(),
477 "warmed {:?}",
478 start.elapsed()
479 );
480 assert!(bench.executions.get() > MIN_WARMUP_RUNS);
481 }
482
483 #[test_log::test]
487 fn one_failed_launch_does_not_end_the_warmup() {
488 let bench = TimedBench::new(Duration::ZERO).failing(1);
489 let start = Instant::now();
490
491 bench.run(TimingMethod::System).expect("the bench runs");
492
493 assert!(start.elapsed() >= bench.warmup_budget(), "left early");
494 }
495
496 #[test_log::test]
499 fn a_launch_that_always_fails_stops_the_warmup() {
500 let bench = TimedBench::new(Duration::ZERO).failing(usize::MAX);
501 let start = Instant::now();
502
503 assert!(bench.run(TimingMethod::System).is_err());
504 assert!(start.elapsed() < bench.warmup_budget(), "kept failing");
505 }
506
507 #[test_log::test]
510 fn a_kernel_that_fills_the_budget_pays_no_extra_launch() {
511 let mut bench = TimedBench::new(Duration::ZERO);
512 bench.per_execution = bench.warmup_budget() / 4;
513
514 let durations = bench.run(TimingMethod::System).expect("the bench runs");
515
516 assert_eq!(
517 bench.executions.get(),
518 MIN_WARMUP_RUNS + durations.durations.len()
519 );
520 }
521
522 #[test_log::test]
523 fn test_min_max_median_durations_even_number_of_samples() {
524 let durations = BenchmarkDurations {
525 timing_method: TimingMethod::System,
526 durations: vec![
527 Duration::new(10, 0),
528 Duration::new(20, 0),
529 Duration::new(30, 0),
530 Duration::new(40, 0),
531 Duration::new(50, 0),
532 ],
533 };
534 let (min, max, median) = durations.min_max_median_durations();
535 assert_eq!(min, Duration::from_secs(10));
536 assert_eq!(max, Duration::from_secs(50));
537 assert_eq!(median, Duration::from_secs(30));
538 }
539
540 #[test_log::test]
541 fn test_min_max_median_durations_odd_number_of_samples() {
542 let durations = BenchmarkDurations {
543 timing_method: TimingMethod::System,
544 durations: vec![
545 Duration::new(18, 5),
546 Duration::new(20, 0),
547 Duration::new(30, 0),
548 Duration::new(40, 0),
549 ],
550 };
551 let (min, max, median) = durations.min_max_median_durations();
552 assert_eq!(min, Duration::from_nanos(18000000005_u64));
553 assert_eq!(max, Duration::from_secs(40));
554 assert_eq!(median, Duration::from_secs(30));
555 }
556
557 #[test_log::test]
558 fn test_mean_duration() {
559 let durations = BenchmarkDurations {
560 timing_method: TimingMethod::System,
561 durations: vec![
562 Duration::new(10, 0),
563 Duration::new(20, 0),
564 Duration::new(30, 0),
565 Duration::new(40, 0),
566 ],
567 };
568 let mean = durations.mean_duration();
569 assert_eq!(mean, Duration::from_secs(25));
570 }
571
572 #[test_log::test]
573 fn test_variance_duration() {
574 let durations = BenchmarkDurations {
575 timing_method: TimingMethod::System,
576 durations: vec![
577 Duration::new(10, 0),
578 Duration::new(20, 0),
579 Duration::new(30, 0),
580 Duration::new(40, 0),
581 Duration::new(50, 0),
582 ],
583 };
584 let mean = durations.mean_duration();
585 let variance = durations.variance_duration(mean);
586 assert_eq!(variance, Duration::from_secs(200));
587 }
588}