use cubecl_core::ir::ElemType;
use cubecl_runtime::{
client::Client,
runtime::Runtime,
throughput::{
MemoryAccess, MemoryCurve, MemoryPoint, MemorySpec, ThroughputError, ThroughputKey,
ThroughputMode, ThroughputValue, sweep_size, working_set_sweep,
},
tune::{AutotuneBound, Bounds, ResourceBound, Thresholds, Work},
};
use crate::throughput::{
Arithmetic, CooperativeMatrix, LaunchConfig, PooledProbes, ShapeSweep, WorkerSweep,
compute_cmma, compute_direct, launch_overhead, memory_direct, memory_probe, memory_read,
memory_write,
};
pub fn device_throughput<R: Runtime>(
device: &R::Device,
keys: &[ThroughputKey],
) -> alloc::vec::Vec<Result<ThroughputValue, ThroughputError>> {
let client = R::client(device);
keys.iter()
.map(|key| measure_peak_throughput(&client, *key))
.collect()
}
pub fn measure_memory_curve(client: &Client, access: MemoryAccess) -> MemoryCurve {
let (points, probed) = {
let _pooled = PooledProbes::enter(client);
sweep(client, access, |bytes| {
ThroughputMode::Memory(MemorySpec::new(access, bytes))
})
};
if probed {
PooledProbes::cleanup_unless_held(client);
}
MemoryCurve::new(access, points)
}
fn sweep(
client: &Client,
access: MemoryAccess,
mode: impl Fn(u64) -> ThroughputMode,
) -> (alloc::vec::Vec<MemoryPoint>, bool) {
let mut probed = false;
let points = working_set_sweep(working_set_cap(client, access))
.into_iter()
.filter_map(|bytes| {
let key = ThroughputKey { mode: mode(bytes) };
let (value, ran) = measure(client, key);
probed |= ran;
Some(MemoryPoint {
bytes,
value: value.ok()?,
})
})
.collect();
(points, probed)
}
fn working_set_cap(client: &Client, access: MemoryAccess) -> u64 {
let max_alloc = client.properties().memory.max_page_size;
memory_probe::window_cap(max_alloc) * access.buffers()
}
pub fn measure_peak_throughput(
client: &Client,
key: ThroughputKey,
) -> Result<ThroughputValue, ThroughputError> {
let (value, probed) = measure(client, key);
if probed {
PooledProbes::cleanup_unless_held(client);
}
value
}
fn measure(
client: &Client,
key: ThroughputKey,
) -> (Result<ThroughputValue, ThroughputError>, bool) {
#[cfg(target_family = "wasm")]
{
let _ = (client, key);
(Err(ThroughputError::Unsupported), false)
}
#[cfg(not(target_family = "wasm"))]
{
let _measurement = cubecl_runtime::dry_run::RealRun::new();
let mut probed = false;
let value = client.measure_throughput(key, || {
probed = true;
probe(client, key)
});
(value, probed)
}
}
fn probe(client: &Client, key: ThroughputKey) -> Result<ThroughputValue, ThroughputError> {
let launch_config = LaunchConfig::for_device(client, key.dtype());
match key.mode {
ThroughputMode::ComputeDirect { dtype } => {
if !client.properties().features.supports_type(dtype) {
return Err(ThroughputError::Unsupported);
}
ShapeSweep::new(compute_direct_shapes(client, dtype, launch_config))
.fastest(|(dtype, config)| Ok(compute_direct::build_kernel(client, dtype, config)))
.map(|(value, _)| value)
}
ThroughputMode::ComputeCmma {
dtype,
config: cmma_config,
} => {
if !CooperativeMatrix::implemented(client, dtype, cmma_config) {
return Err(ThroughputError::Unsupported);
}
ShapeSweep::new(alloc::vec![launch_config])
.fastest(|config| Ok(compute_cmma::build_kernel(client, key, cmma_config, config)))
.map(|(value, _)| value)
}
ThroughputMode::Memory(spec) => {
let (value, fastest) = ShapeSweep::new(WorkerSweep::shapes(
client,
launch_config,
spec.access,
))
.fastest(|config| match spec.access {
MemoryAccess::Copy => memory_direct::build_kernel(client, key, config, spec),
MemoryAccess::Read => memory_read::build_kernel(client, key, config, spec),
MemoryAccess::Write => memory_write::build_kernel(client, key, config, spec),
})?;
WorkerSweep::remember(client, spec.access, fastest.cube_dim.num_elems());
Ok(value)
}
ThroughputMode::Launch => ShapeSweep::new(alloc::vec![launch_config])
.fastest(|config| Ok(launch_overhead::build_kernel(client, key, config)))
.map(|(value, _)| value),
}
}
fn compute_direct_shapes(
client: &Client,
dtype: ElemType,
launch_config: LaunchConfig,
) -> alloc::vec::Vec<(ElemType, LaunchConfig)> {
Arithmetic::dtypes(client, dtype)
.into_iter()
.flat_map(|dtype| {
Arithmetic::widths(client, dtype)
.into_iter()
.map(move |vector_size| {
(
dtype,
LaunchConfig {
vector_size,
..launch_config
},
)
})
})
.collect()
}
pub fn roofline_bounds(
client: &Client,
compute_key: ThroughputKey,
work: Work,
thresholds: Thresholds,
) -> Bounds {
Bounds {
bounds: alloc::vec![
compute_bound(client, compute_key, work, thresholds.compute),
memory_bound(client, MemoryAccess::Copy, work, thresholds.memory),
],
launch_overhead: measure_launch_overhead(client),
}
}
pub fn compute_bound(
client: &Client,
compute_key: ThroughputKey,
work: Work,
threshold: f32,
) -> AutotuneBound {
let peak = measure_peak_throughput(client, compute_key).unwrap_or(ThroughputValue::ZERO);
AutotuneBound {
resource: ResourceBound {
amount: work.compute_ops,
peak_per_s: peak.ops_per_s(),
},
threshold,
}
}
pub fn memory_bound(
client: &Client,
access: MemoryAccess,
work: Work,
threshold: f32,
) -> AutotuneBound {
let footprint = (work.bytes as u64).min(working_set_cap(client, access));
let memory_key = ThroughputKey {
mode: ThroughputMode::Memory(MemorySpec::new(access, sweep_size(footprint))),
};
let peak = measure_peak_throughput(client, memory_key).unwrap_or(ThroughputValue::ZERO);
AutotuneBound {
resource: ResourceBound {
amount: work.bytes,
peak_per_s: peak.bytes_per_s(&memory_key),
},
threshold,
}
}
pub fn measure_launch_overhead(client: &Client) -> core::time::Duration {
let launch_key = ThroughputKey {
mode: ThroughputMode::Launch,
};
measure_peak_throughput(client, launch_key)
.map(|value| value.duration_per_op())
.unwrap_or_default()
}