use super::{Benchmark, BenchmarkComputations, BenchmarkDurations, TimingMethod};
use crate::client::Client;
use alloc::{format, string::String, vec::Vec};
use core::time::Duration;
use cubecl_common::profile::{Instant, ProfileDuration};
use cubecl_environment::future::block_on;
const MIN_WARMUP_RUNS: usize = 5;
pub struct MultiDeviceRun<'a, B: Benchmark> {
benchmark: &'a B,
clients: &'a [Client],
}
impl<'a, B: Benchmark> MultiDeviceRun<'a, B> {
pub fn new(benchmark: &'a B, clients: &'a [Client]) -> Self {
Self { benchmark, clients }
}
pub fn run(&self) -> Result<MultiDeviceDurations, String> {
let input = self.benchmark.prepare();
let budget = self.benchmark.warmup_budget();
let warmup = Instant::now();
let mut warmups = 0;
while warmups < MIN_WARMUP_RUNS || warmup.elapsed() < budget {
self.sample(&input)?;
warmups += 1;
}
let samples = (0..self.benchmark.num_samples())
.map(|_| self.sample(&input))
.collect::<Result<Vec<_>, _>>()?;
Ok(MultiDeviceDurations::new(samples))
}
fn sample(&self, input: &B::Input) -> Result<MultiDeviceSample, String> {
let clients = self.clients;
sync_all(clients)?;
let started = Instant::now();
let windows = clients
.iter()
.map(|client| client.profile_start())
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("{error:?}"))?;
let output = self.benchmark.execute(input.clone());
let profiles = clients
.iter()
.zip(windows)
.map(|(client, window)| client.profile_end(window))
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("{error:?}"));
sync_all(clients)?;
let wall = started.elapsed();
drop(output?);
let profiles = profiles?;
let timing_method = profiles
.first()
.map_or(TimingMethod::System, ProfileDuration::timing_method);
let members = profiles
.into_iter()
.map(|profile| {
block_on(profile.resolve())
.map(|ticks| ticks.duration())
.ok_or_else(|| String::from("a member's window carried no measurement"))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(MultiDeviceSample {
members,
wall,
timing_method,
})
}
}
#[derive(Debug, Clone)]
pub struct MultiDeviceSample {
pub members: Vec<Duration>,
pub wall: Duration,
pub timing_method: TimingMethod,
}
#[derive(Debug, Clone)]
pub struct MultiDeviceDurations {
pub members: Vec<BenchmarkDurations>,
pub slowest: BenchmarkDurations,
pub wall: BenchmarkDurations,
}
impl MultiDeviceDurations {
pub fn new(samples: Vec<MultiDeviceSample>) -> Self {
let first = samples
.first()
.expect("a multi-device benchmark takes samples");
let timing_method = first.timing_method;
let count = first.members.len();
assert!(
samples.iter().all(|sample| sample.members.len() == count),
"every sample of a multi-device benchmark has the same members"
);
let column = |pick: &dyn Fn(&MultiDeviceSample) -> Duration| {
BenchmarkDurations::from_durations(timing_method, samples.iter().map(pick).collect())
};
Self {
members: (0..count)
.map(|member| column(&|sample| sample.members[member]))
.collect(),
slowest: column(&|sample| sample.members.iter().copied().max().unwrap_or_default()),
wall: BenchmarkDurations::from_durations(
TimingMethod::System,
samples.iter().map(|sample| sample.wall).collect(),
),
}
}
pub fn median(&self) -> Duration {
BenchmarkComputations::new(&self.slowest).median
}
pub fn spread(&self) -> f64 {
let computed = BenchmarkComputations::new(&self.slowest);
computed.max.as_secs_f64() / computed.min.as_secs_f64().max(f64::MIN_POSITIVE)
}
}
fn sync_all(clients: &[Client]) -> Result<(), String> {
clients
.iter()
.try_for_each(|client| block_on(client.sync()).map_err(|error| format!("{error:?}")))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn sample(members: &[u64], wall: u64) -> MultiDeviceSample {
MultiDeviceSample {
members: members
.iter()
.map(|&us| Duration::from_micros(us))
.collect(),
wall: Duration::from_micros(wall),
timing_method: TimingMethod::Device,
}
}
#[test]
fn the_slowest_member_of_each_sample_bounds_it() {
let durations = MultiDeviceDurations::new(vec![
sample(&[10, 30], 40),
sample(&[50, 20], 60),
sample(&[15, 25], 30),
]);
let slowest: Vec<u64> = durations
.slowest
.durations
.iter()
.map(|d| d.as_micros() as u64)
.collect();
assert_eq!(slowest, [30, 50, 25]);
assert_eq!(durations.members.len(), 2);
assert_eq!(durations.median(), Duration::from_micros(30));
assert_eq!(durations.spread(), 2.0);
}
}