use cubecl_core as cubecl;
use cubecl_core::prelude::*;
use cubecl_core::server::Handle;
use cubecl_wgpu::WgpuRuntime;
use std::time::{Duration, Instant};
#[cube(launch)]
fn add_one_tensor(input: &Tensor<f32>, output: &mut Tensor<f32>) {
if ABSOLUTE_POS < input.shape(0) {
output[ABSOLUTE_POS] = input[ABSOLUTE_POS] + 1.0;
}
}
struct Config {
name: &'static str,
elems: usize,
kernels: usize,
rounds: usize,
}
const CONFIGS: &[Config] = &[
Config {
name: "tiny x150",
elems: 64,
kernels: 150,
rounds: 100,
},
Config {
name: "tiny x1000",
elems: 64,
kernels: 1000,
rounds: 50,
},
Config {
name: "tiny x5000",
elems: 64,
kernels: 5000,
rounds: 10,
},
Config {
name: "64k x1000",
elems: 64 * 1024,
kernels: 1000,
rounds: 20,
},
Config {
name: "256k x1000",
elems: 256 * 1024,
kernels: 1000,
rounds: 10,
},
Config {
name: "1m x500",
elems: 1024 * 1024,
kernels: 500,
rounds: 10,
},
Config {
name: "1m x2000",
elems: 1024 * 1024,
kernels: 2000,
rounds: 5,
},
];
const CUBE_DIM: u32 = 256;
fn main() {
let client = WgpuRuntime::client(&Default::default());
println!("adapter: {}", WgpuRuntime::name(&client));
println!(
"{:>11} | {:>19} | {:>24} | {:>6} | {:>8}",
"config", "enqueue µs/pass", "e2e µs/kernel", "share", "capture"
);
println!(
"{:>11} | {:>9} {:>9} | {:>9} {:>9} {:>4}| {:>6} | {:>8}",
"", "normal", "replay", "normal", "replay", "x", "", ""
);
for config in CONFIGS {
bench(&client, config);
}
}
fn bench(client: &ComputeClient<WgpuRuntime>, config: &Config) {
let a = client.create_from_slice(f32::as_bytes(&vec![0.0f32; config.elems]));
let b = client.create_from_slice(f32::as_bytes(&vec![0.0f32; config.elems]));
run_pass(client, &a, &b, config);
sync(client, &a);
client.graph_prepare().expect("graph_prepare");
run_pass(client, &a, &b, config);
sync(client, &a);
client.start_capture().expect("start_capture");
run_pass(client, &a, &b, config);
let capture_start = Instant::now();
let graph = client.stop_capture().expect("stop_capture");
let capture = capture_start.elapsed();
unsafe { graph.replay() };
sync(client, &a);
let mut normal = Measure::default();
let mut replay = Measure::default();
for _ in 0..config.rounds {
normal.round(|| run_pass(client, &a, &b, config), || sync(client, &a));
replay.round(|| unsafe { graph.replay() }, || sync(client, &a));
}
let launches = (config.rounds * config.kernels) as f64;
let passes = config.rounds as f64;
let share = normal.issue.as_secs_f64() / normal.total.as_secs_f64();
println!(
"{:>11} | {:>9.2} {:>9.2} | {:>9.2} {:>9.2} {:>3.1}x| {:>5.1}% | {:>8.2?}",
config.name,
per(normal.issue, passes),
per(replay.issue, passes),
per(normal.total, launches),
per(replay.total, launches),
ratio(normal.total, replay.total),
share * 100.0,
capture,
);
}
#[derive(Default)]
struct Measure {
issue: Duration,
total: Duration,
}
impl Measure {
fn round(&mut self, issue: impl FnOnce(), sync: impl FnOnce()) {
let start = Instant::now();
issue();
self.issue += start.elapsed();
sync();
self.total += start.elapsed();
}
}
fn run_pass(client: &ComputeClient<WgpuRuntime>, a: &Handle, b: &Handle, config: &Config) {
let cubes = config.elems.div_ceil(CUBE_DIM as usize) as u32;
for i in 0..config.kernels {
let (src, dst) = if i % 2 == 0 { (a, b) } else { (b, a) };
add_one_tensor::launch(
client,
CubeCount::Static(cubes, 1, 1),
CubeDim::new_1d(CUBE_DIM),
unsafe { TensorArg::from_raw_parts(src.clone(), [1].into(), [config.elems].into()) },
unsafe { TensorArg::from_raw_parts(dst.clone(), [1].into(), [config.elems].into()) },
);
}
}
fn sync(client: &ComputeClient<WgpuRuntime>, handle: &Handle) {
let _ = client.read_one(handle.clone()).unwrap();
}
fn per(elapsed: Duration, count: f64) -> f64 {
elapsed.as_secs_f64() * 1e6 / count
}
fn ratio(before: Duration, after: Duration) -> f64 {
before.as_secs_f64() / after.as_secs_f64()
}