use rlx_driver::Device;
use rlx_ir::{Graph, Tick};
use rlx_runtime::{GpuThermal, Session, device_thermal};
pub mod patterns;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
L1,
L2,
}
pub trait BenchmarkPattern {
fn name(&self) -> &str;
fn tier(&self) -> Tier;
fn build_graph(&self) -> Graph;
fn input_data(&self) -> Vec<(String, Vec<f32>)>;
fn param_data(&self) -> Vec<(String, Vec<f32>)> {
Vec::new()
}
}
#[derive(Debug, Clone)]
pub struct BenchResult {
pub pattern: String,
pub tier: Tier,
pub device: Device,
pub n_runs: usize,
pub samples_ns: Vec<u64>,
pub gpu_peak: Option<GpuPeak>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GpuPeak {
pub name: Option<String>,
pub temp_c: Option<f32>,
pub power_w: Option<f32>,
}
impl BenchResult {
pub fn min_ns(&self) -> u64 {
self.samples_ns.iter().copied().min().unwrap_or(0)
}
pub fn max_ns(&self) -> u64 {
self.samples_ns.iter().copied().max().unwrap_or(0)
}
pub fn mean_ns(&self) -> u64 {
if self.samples_ns.is_empty() {
return 0;
}
let sum: u128 = self.samples_ns.iter().map(|&v| v as u128).sum();
(sum / self.samples_ns.len() as u128) as u64
}
pub fn median_ns(&self) -> u64 {
if self.samples_ns.is_empty() {
return 0;
}
let mut sorted = self.samples_ns.clone();
sorted.sort_unstable();
sorted[sorted.len() / 2]
}
}
impl std::fmt::Display for BenchResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let to_us = |ns: u64| ns as f64 / 1000.0;
write!(
f,
"{:?}/{:?} {:?}: n={} mean={:.2}µs median={:.2}µs min={:.2}µs max={:.2}µs",
self.tier,
self.device,
self.pattern,
self.n_runs,
to_us(self.mean_ns()),
to_us(self.median_ns()),
to_us(self.min_ns()),
to_us(self.max_ns()),
)?;
if let Some(g) = &self.gpu_peak {
if let Some(t) = g.temp_c {
write!(f, " gpu_peak={t:.0}°C")?;
}
if let Some(p) = g.power_w {
write!(f, " {p:.0}W")?;
}
}
Ok(())
}
}
fn gpu_bench_sample(device: Device) -> Option<GpuThermal> {
match device {
Device::Cuda | Device::Rocm => device_thermal(device, 0),
_ => None,
}
}
fn max_opt(a: Option<f32>, b: Option<f32>) -> Option<f32> {
match (a, b) {
(Some(x), Some(y)) => Some(x.max(y)),
(x, None) => x,
(None, y) => y,
}
}
fn merge_gpu_peak(a: Option<GpuThermal>, b: Option<GpuThermal>) -> Option<GpuPeak> {
if a.is_none() && b.is_none() {
return None;
}
let name = b
.as_ref()
.and_then(|x| x.name.clone())
.or_else(|| a.as_ref().and_then(|x| x.name.clone()));
let temp_c = max_opt(
a.as_ref().and_then(|x| x.temp_c),
b.as_ref().and_then(|x| x.temp_c),
);
let power_w = max_opt(
a.as_ref().and_then(|x| x.power_w),
b.as_ref().and_then(|x| x.power_w),
);
Some(GpuPeak {
name,
temp_c,
power_w,
})
}
pub fn run_benchmark<P: BenchmarkPattern>(
pattern: &P,
device: Device,
n_warmup: usize,
n_runs: usize,
) -> BenchResult {
let graph = pattern.build_graph();
let mut compiled = Session::new(device).compile(graph);
for (name, data) in pattern.param_data() {
compiled.set_param(&name, &data);
}
let inputs_owned = pattern.input_data();
let inputs: Vec<(&str, &[f32])> = inputs_owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
for _ in 0..n_warmup {
let _ = compiled.run(&inputs);
}
let gpu_before = gpu_bench_sample(device);
let mut samples_ns = Vec::with_capacity(n_runs);
let tick = Tick::now();
for _ in 0..n_runs {
let t0 = Tick::now();
let _ = compiled.run(&inputs);
let elapsed = Tick::now().elapsed_ns(t0);
samples_ns.push(elapsed);
}
let _ = tick;
let gpu_after = gpu_bench_sample(device);
BenchResult {
pattern: pattern.name().to_string(),
tier: pattern.tier(),
device,
n_runs,
samples_ns,
gpu_peak: merge_gpu_peak(gpu_before, gpu_after),
}
}
pub fn run_benchmark_dispatch_only<P: BenchmarkPattern>(
pattern: &P,
device: Device,
n_warmup: usize,
n_runs: usize,
) -> BenchResult {
unsafe {
std::env::set_var("RLX_BENCH_DISPATCH_ONLY", "1");
}
let r = run_benchmark(pattern, device, n_warmup, n_runs);
unsafe {
std::env::remove_var("RLX_BENCH_DISPATCH_ONLY");
}
r
}
#[cfg(test)]
mod tests {
use super::*;
use crate::patterns::*;
#[test]
fn matmul_pattern_runs_on_cpu() {
let p = MatmulPattern { m: 8, k: 16, n: 8 };
let r = run_benchmark(&p, Device::Cpu, 1, 3);
assert_eq!(r.pattern, "matmul");
assert_eq!(r.tier, Tier::L1);
assert_eq!(r.device, Device::Cpu);
assert_eq!(r.n_runs, 3);
assert_eq!(r.samples_ns.len(), 3);
assert!(r.samples_ns.iter().all(|&v| v > 0));
}
#[test]
fn layernorm_pattern_runs_on_cpu() {
let p = LayerNormPattern {
rows: 8,
hidden: 16,
};
let r = run_benchmark(&p, Device::Cpu, 1, 3);
assert_eq!(r.pattern, "layer_norm");
assert_eq!(r.tier, Tier::L1);
assert_eq!(r.samples_ns.len(), 3);
}
#[test]
fn matmul_bias_relu_l2_pattern_runs_on_cpu() {
let p = MatmulBiasReluPattern { m: 8, k: 16, n: 8 };
let r = run_benchmark(&p, Device::Cpu, 1, 3);
assert_eq!(r.pattern, "matmul_bias_relu");
assert_eq!(r.tier, Tier::L2);
assert_eq!(r.samples_ns.len(), 3);
}
#[test]
fn bench_result_stats_are_sane() {
let r = BenchResult {
pattern: "x".into(),
tier: Tier::L1,
device: Device::Cpu,
n_runs: 5,
samples_ns: vec![100, 200, 300, 400, 500],
gpu_peak: None,
};
assert_eq!(r.min_ns(), 100);
assert_eq!(r.max_ns(), 500);
assert_eq!(r.mean_ns(), 300);
assert_eq!(r.median_ns(), 300);
}
#[test]
fn bench_result_display_is_human_readable() {
let r = BenchResult {
pattern: "matmul".into(),
tier: Tier::L1,
device: Device::Cpu,
n_runs: 2,
samples_ns: vec![1000, 2000],
gpu_peak: None,
};
let s = format!("{r}");
assert!(s.contains("matmul"));
assert!(s.contains("L1"));
assert!(s.contains("Cpu"));
assert!(s.contains("n=2"));
}
}