use crate::throughput::{CmmaDims, ComputeCmmaConfig};
use alloc::{format, string::String};
use core::time::Duration;
use cubecl_ir::{ElemType, FloatKind};
use thiserror::Error;
pub const PROBE_VERSION: u32 = 2;
pub const DEFAULT_WORKING_SET_BYTES: u64 = 512 * 1024 * 1024;
#[derive(Eq, PartialEq, Clone, Hash, Debug, Copy)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum MemoryAccess {
Copy,
Read,
Write,
}
impl MemoryAccess {
pub const fn buffers(&self) -> u64 {
match self {
Self::Copy => 2,
Self::Read | Self::Write => 1,
}
}
pub const fn default_working_set(&self) -> u64 {
DEFAULT_WORKING_SET_BYTES * self.buffers()
}
}
#[derive(Eq, PartialEq, Clone, Hash, Debug, Copy)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum ThroughputMode {
ComputeDirect {
dtype: ElemType,
},
ComputeCmma {
dtype: ElemType,
config: ComputeCmmaConfig,
},
Memory(MemorySpec),
Launch,
}
#[derive(Eq, PartialEq, Clone, Hash, Debug, Copy)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(serializable, serde(deny_unknown_fields))]
pub struct MemorySpec {
pub access: MemoryAccess,
pub bytes: u64,
}
impl ThroughputMode {
pub const fn memory(access: MemoryAccess) -> Self {
Self::Memory(MemorySpec::new(access, access.default_working_set()))
}
pub const fn memory_probe(&self) -> Option<MemorySpec> {
match self {
Self::Memory(spec) => Some(*spec),
Self::ComputeDirect { .. } | Self::ComputeCmma { .. } | Self::Launch => None,
}
}
}
impl MemorySpec {
pub const fn new(access: MemoryAccess, bytes: u64) -> Self {
Self { access, bytes }
}
}
#[derive(Eq, PartialEq, Clone, Hash, Debug, Copy)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(serializable, serde(deny_unknown_fields))]
pub struct ThroughputKey {
pub mode: ThroughputMode,
}
impl ThroughputKey {
pub fn dtype(&self) -> ElemType {
match self.mode {
ThroughputMode::ComputeDirect { dtype } => dtype,
ThroughputMode::ComputeCmma { dtype, .. } => dtype,
ThroughputMode::Memory(_) | ThroughputMode::Launch => ElemType::Float(FloatKind::F32),
}
}
}
#[derive(Error, Eq, PartialEq, Clone, Copy, Debug)]
pub enum ThroughputError {
#[error("unsupported")]
Unsupported,
#[error("no timing")]
NoTiming,
#[error("allocation failed")]
Allocation,
#[error("launch failed")]
Launch,
}
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub struct ThroughputValue {
pub ops_count: usize,
pub duration: Duration,
}
impl ThroughputValue {
pub const ZERO: Self = Self {
ops_count: 0,
duration: Duration::ZERO,
};
pub fn ops_per_s(&self) -> f64 {
if self.duration.is_zero() {
return f64::NAN;
}
self.ops_count as f64 / self.duration.as_secs_f64()
}
pub fn bytes_per_s(&self, key: &ThroughputKey) -> f64 {
if self.duration.is_zero() {
return f64::NAN;
}
(self.ops_count * key.dtype().size()) as f64 / self.duration.as_secs_f64()
}
pub fn duration_per_op(&self) -> Duration {
if self.ops_count == 0 {
Duration::ZERO
} else {
Duration::from_secs_f64(self.duration.as_secs_f64() / self.ops_count as f64)
}
}
pub fn format(&self, key: &ThroughputKey) -> String {
let (mut val_per_s, unit) = match key.mode {
ThroughputMode::ComputeDirect { .. } | ThroughputMode::ComputeCmma { .. } => {
(self.ops_per_s(), "OPS")
}
ThroughputMode::Memory(_) => (self.bytes_per_s(key), "bytes"),
ThroughputMode::Launch => {
let dur = self.duration_per_op();
if dur.is_zero() {
return String::from("N/A");
}
return format!("{dur:?}/launch");
}
};
if val_per_s.is_nan() {
return String::from("N/A");
}
let suffixes = ["", "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q"];
let mut suffix_idx = 0;
for _ in 0..suffixes.len() - 1 {
if val_per_s < 1000.0 {
break;
}
val_per_s /= 1000.0;
suffix_idx += 1;
}
format!("{val_per_s:.4} {}{unit}/s", suffixes[suffix_idx])
}
}
pub fn compute_throughput_key(
cmma_tile: Option<(u32, u32, u32)>,
input_elem_type: ElemType,
acc_elem_type: ElemType,
) -> ThroughputKey {
let mode = match cmma_tile {
Some((tile_m, tile_n, tile_k)) => ThroughputMode::ComputeCmma {
dtype: input_elem_type,
config: ComputeCmmaConfig {
accumulator_type: acc_elem_type,
cmma_dims: CmmaDims {
m: tile_m as usize,
n: tile_n as usize,
k: tile_k as usize,
},
},
},
None => ThroughputMode::ComputeDirect {
dtype: acc_elem_type,
},
};
ThroughputKey { mode }
}
#[cfg(all(test, std_io))]
mod tests {
use super::*;
#[test]
fn a_memory_key_keeps_its_serialized_form() {
let encode = |mode| serde_json::to_string(&ThroughputKey { mode }).unwrap();
assert_eq!(
encode(ThroughputMode::memory(MemoryAccess::Copy)),
r#"{"mode":{"Memory":{"access":"Copy","bytes":1073741824}}}"#
);
assert_eq!(
encode(ThroughputMode::Memory(MemorySpec::new(
MemoryAccess::Read,
8192
))),
r#"{"mode":{"Memory":{"access":"Read","bytes":8192}}}"#
);
}
#[test]
fn a_working_set_is_part_of_the_key() {
let small = ThroughputMode::Memory(MemorySpec::new(MemoryAccess::Read, 8192));
let large = ThroughputMode::Memory(MemorySpec::new(MemoryAccess::Read, 16384));
assert_ne!(small, large);
}
}