use cubecl::{
benchmark::{Benchmark, ProfileDuration, TimingMethod},
client::Client,
future,
ir::ElemType,
prelude::*,
std::tensor::TensorHandle,
zspace::Shape,
};
use cubek_test_utils::{RunSamples, TestInput};
use crate::ReduceStrategy;
use crate::components::instructions::ReduceOperationConfig;
use crate::eval::benchmarks::correctness::ReduceCorrectness;
use crate::eval::benchmarks::problem::{ReduceBenchKind, ReduceProblem};
pub fn bench(
strategy: &ReduceStrategy,
problem: &ReduceProblem,
num_samples: usize,
) -> Result<RunSamples, String> {
ReduceCorrectness::verify(strategy, problem)?;
let device = cubecl::test_device();
let client = device.client();
let bench = ReduceBench {
shape: problem.shape.clone(),
axis: problem.axis,
config: problem.config,
kind: problem.kind,
value_dtype: problem.precision.dtype(),
strategy: strategy.clone(),
device,
client,
samples: num_samples,
};
let durations = bench
.run(cubek_test_utils::timing_method(TimingMethod::Device))
.map_err(|e| format!("benchmark failed: {e}"))?
.durations;
Ok(RunSamples::new(durations))
}
fn two_launch_configs(
config: ReduceOperationConfig,
) -> (ReduceOperationConfig, ReduceOperationConfig) {
match config {
ReduceOperationConfig::TopK(k) | ReduceOperationConfig::ArgTopK(k) => (
ReduceOperationConfig::TopK(k),
ReduceOperationConfig::ArgTopK(k),
),
ReduceOperationConfig::Max | ReduceOperationConfig::ArgMax => {
(ReduceOperationConfig::Max, ReduceOperationConfig::ArgMax)
}
ReduceOperationConfig::Min | ReduceOperationConfig::ArgMin => {
(ReduceOperationConfig::Min, ReduceOperationConfig::ArgMin)
}
other => panic!("{other:?} has no values/indices pair to compare"),
}
}
#[derive(Copy, Clone, PartialEq)]
enum InputOrder {
Uniform,
Ascending,
Descending,
}
impl InputOrder {
fn from_env() -> Self {
match std::env::var("CUBEK_BENCH_INPUT").as_deref() {
Ok("ascending") => InputOrder::Ascending,
Ok("descending") => InputOrder::Descending,
Ok("uniform") | Err(_) => InputOrder::Uniform,
Ok(other) => {
panic!("CUBEK_BENCH_INPUT takes uniform, ascending or descending, got {other:?}")
}
}
}
fn data(self, shape: &[usize], axis: usize) -> Vec<f32> {
let axis_len = shape[axis];
let inner: usize = shape[axis + 1..].iter().product();
let last = (axis_len - 1) as f32;
(0..shape.iter().product::<usize>())
.map(|linear| {
let coordinate = (linear / inner) % axis_len;
match self {
InputOrder::Ascending => coordinate as f32,
InputOrder::Descending => last - coordinate as f32,
InputOrder::Uniform => unreachable!("uniform does not build its data here"),
}
})
.collect()
}
}
struct ReduceBench {
shape: Vec<usize>,
axis: usize,
config: ReduceOperationConfig,
kind: ReduceBenchKind,
value_dtype: ElemType,
strategy: ReduceStrategy,
device: cubecl::Device,
client: Client,
samples: usize,
}
impl Benchmark for ReduceBench {
type Input = (TensorHandle, TensorHandle, TensorHandle);
type Output = ();
fn prepare(&self) -> Self::Input {
let client = self.device.client();
let elem = self.value_dtype;
let output_elem = crate::eval::cpu_reference::output_dtype_for(&self.config, elem);
let builder =
TestInput::builder(client.clone(), Shape::from(self.shape.clone())).dtype(elem);
let input = match InputOrder::from_env() {
InputOrder::Uniform => builder.uniform(0, 0., 1.),
order => builder.custom(order.data(&self.shape, self.axis)),
}
.generate_without_host_data();
let mut shape_out = self.shape.clone();
let reduce_len = match self.config {
ReduceOperationConfig::ArgTopK(len) => len,
ReduceOperationConfig::TopK(len) => len,
_ => 1,
};
shape_out[self.axis] = reduce_len;
let out = TensorHandle::empty(&client, shape_out.clone(), output_elem);
let indices = TensorHandle::empty(&client, shape_out, u32::elem_type_native());
(input, out, indices)
}
fn execute(&self, (input, out, indices): Self::Input) -> Result<(), String> {
let value_dtype = self.value_dtype;
let index_dtype = u32::elem_type_native();
let acc_dtype = crate::eval::cpu_reference::accumulation_dtype();
match self.kind {
ReduceBenchKind::Single => {
let output_dtype =
crate::eval::cpu_reference::output_dtype_for(&self.config, value_dtype);
crate::reduce(
&self.client,
input.binding(),
out.binding(),
self.axis,
self.strategy.clone(),
self.config,
crate::ReduceDtypes {
input: value_dtype,
output: output_dtype,
accumulation: acc_dtype,
},
)
.map_err(|err| format!("{err}"))?;
}
ReduceBenchKind::TwoLaunch => {
let (values_config, indices_config) = two_launch_configs(self.config);
crate::reduce(
&self.client,
input.clone().binding(),
out.binding(),
self.axis,
self.strategy.clone(),
values_config,
crate::ReduceDtypes {
input: value_dtype,
output: value_dtype,
accumulation: acc_dtype,
},
)
.map_err(|err| format!("{err}"))?;
crate::reduce(
&self.client,
input.binding(),
indices.binding(),
self.axis,
self.strategy.clone(),
indices_config,
crate::ReduceDtypes {
input: value_dtype,
output: index_dtype,
accumulation: acc_dtype,
},
)
.map_err(|err| format!("{err}"))?;
}
ReduceBenchKind::Fused => {
crate::reduce_with_indices(
&self.client,
input.binding(),
out.binding(),
indices.binding(),
self.axis,
self.strategy.clone(),
self.config,
crate::ReduceWithIndicesDtypes {
input: value_dtype,
values: value_dtype,
indices: index_dtype,
accumulation: acc_dtype,
},
)
.map_err(|err| format!("{err}"))?;
}
}
Ok(())
}
fn profile(&self, args: Self::Input) -> Result<ProfileDuration, String> {
cubek_test_utils::profile_launch(&self.client, "reduce-bench", || self.execute(args))
}
fn num_samples(&self) -> usize {
self.samples
}
fn name(&self) -> String {
format!(
"reduce-axis({})-{}-{:?}-{:?}-{:?}-{:?}",
self.axis, self.value_dtype, self.shape, self.strategy, self.config, self.kind,
)
.to_lowercase()
}
fn sync(&self) {
future::block_on(self.client.sync()).unwrap()
}
}
#[cfg(test)]
mod tests {
use super::InputOrder;
fn rows(order: InputOrder, shape: &[usize], axis: usize) -> Vec<Vec<f32>> {
let data = order.data(shape, axis);
let inner: usize = shape[axis + 1..].iter().product();
let outer: usize = shape[..axis].iter().product();
let mut rows = Vec::new();
for o in 0..outer {
for i in 0..inner {
let start = o * shape[axis] * inner + i;
rows.push((0..shape[axis]).map(|c| data[start + c * inner]).collect());
}
}
rows
}
#[test]
fn ascending_rises_along_the_reduce_axis() {
for row in rows(InputOrder::Ascending, &[2, 3, 4], 1) {
assert_eq!(row, vec![0.0, 1.0, 2.0]);
}
}
#[test]
fn descending_falls_along_the_reduce_axis() {
for row in rows(InputOrder::Descending, &[2, 3, 4], 1) {
assert_eq!(row, vec![2.0, 1.0, 0.0]);
}
}
#[test]
fn every_value_in_a_row_is_distinct_at_the_benchmark_shape() {
let shape = [32, 512, 4095];
let data = InputOrder::Ascending.data(&shape, 2);
let row: Vec<f32> = data[shape[2] * 700..shape[2] * 701].to_vec();
assert!(row.windows(2).all(|w| w[1] > w[0]));
assert_eq!(row[0], 0.0);
assert_eq!(row[shape[2] - 1], (shape[2] - 1) as f32);
}
}