use cubecl::prelude::*;
use cubecl_core::{self as cubecl, ir::ElemType};
use cubecl_runtime::{
server::Handle,
throughput::{DEFAULT_WORKING_SET_BYTES, MemorySpec, ThroughputError},
};
use crate::throughput::LaunchConfig;
use cubecl_common::profile::Duration;
const POOL_WINDOWS: usize = 2;
pub(crate) fn window_cap(max_alloc: u64) -> u64 {
(DEFAULT_WORKING_SET_BYTES.min(max_alloc / POOL_WINDOWS as u64)).max(1)
}
#[derive(Clone, Copy, Debug)]
pub struct MemoryProbe {
pub pool_lines: usize,
pub window_lines: usize,
pub buffer_bytes: usize,
pub cube_count: usize,
pub blocked: bool,
}
#[derive(Clone, Copy)]
struct DeviceShape {
max_alloc: usize,
cube_dim: usize,
cube_count: usize,
}
impl MemoryProbe {
pub fn min_iterations(&self) -> usize {
self.pool_lines.div_ceil(self.window_lines)
}
pub fn new(client: &Client, config: LaunchConfig, line_bytes: usize, spec: MemorySpec) -> Self {
let blocked = config.plane_size == 1;
let shape = DeviceShape {
max_alloc: client.properties().memory.max_page_size as usize,
cube_dim: config.cube_dim.num_elems() as usize,
cube_count: if blocked { 1 } else { config.cube_count },
};
Self::sized(shape, line_bytes, spec, blocked)
}
fn sized(shape: DeviceShape, line_bytes: usize, spec: MemorySpec, blocked: bool) -> Self {
let buffers = spec.access.buffers() as usize;
let window_bytes = (spec.bytes.min(usize::MAX as u64) as usize) / buffers;
let cap_lines = (window_cap(shape.max_alloc as u64) as usize / line_bytes).max(1);
let window_lines = (window_bytes / line_bytes).clamp(1, cap_lines);
let pool_lines = cap_lines * POOL_WINDOWS;
let cube_count = (window_lines / shape.cube_dim).clamp(1, shape.cube_count);
Self {
pool_lines,
window_lines,
buffer_bytes: pool_lines * line_bytes,
cube_count,
blocked,
}
}
}
pub fn reserve<const N: usize>(
client: &Client,
bytes: [usize; N],
) -> Result<[Handle; N], ThroughputError> {
let handles =
client.memory_persistent_allocation((), |_| bytes.map(|bytes| client.empty(bytes)));
client
.check(&handles)
.map_err(|_| ThroughputError::Allocation)?;
Ok(handles)
}
pub fn verify(
client: &Client,
sample: impl Fn(usize) -> Duration,
written: &Handle,
) -> Result<(), ThroughputError> {
sample(1);
cubecl_core::future::block_on(client.sync_buffers([written]))
.map_err(|_| ThroughputError::Launch)
}
pub fn prime(
client: &Client,
handle: &Handle,
pool_lines: usize,
config: LaunchConfig,
dtype: ElemType,
) {
unsafe {
prime_buffer::launch_unchecked(
client,
CubeCount::Static(config.cube_count as u32, 1, 1),
config.cube_dim,
config.vector_size,
BufferArg::from_raw_parts(handle.clone(), pool_lines),
pool_lines,
dtype,
);
}
let _ = cubecl_core::future::block_on(client.sync());
}
#[cube(launch_unchecked)]
fn prime_buffer<I: Numeric, N: Size>(
output: &mut [Vector<I, N>],
len: usize,
#[define(I)] _dtype: ElemType,
) {
let stride = CUBE_DIM as usize * CUBE_COUNT;
let steps = len.div_ceil(stride).max(1);
for step in 0..steps {
let idx = ABSOLUTE_POS + step * stride;
if idx < len {
output[idx] = Vector::<I, N>::empty();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cubecl_runtime::throughput::MemoryAccess;
const KB: usize = 1024;
const MB: usize = 1024 * 1024;
const DEVICE: DeviceShape = DeviceShape {
max_alloc: 1024 * MB,
cube_dim: 256,
cube_count: 2048,
};
fn probe(working_set: usize, access: MemoryAccess) -> MemoryProbe {
let spec = MemorySpec::new(access, working_set as u64);
MemoryProbe::sized(DEVICE, 16, spec, false)
}
#[test]
fn the_pool_stays_large_however_small_the_window() {
let small = probe(256 * KB, MemoryAccess::Read);
assert_eq!(small.pool_lines, 1024 * MB / 16);
assert_eq!(small.window_lines, 256 * KB / 16);
let copy = probe(256 * KB, MemoryAccess::Copy);
assert_eq!(copy.pool_lines, 1024 * MB / 16);
assert_eq!(copy.window_lines, 128 * KB / 16);
}
#[test]
fn no_window_stops_walking() {
for bytes in [256 * KB, 64 * MB, 256 * MB, 512 * MB, 8 * 1024 * MB] {
let probe = probe(bytes, MemoryAccess::Read);
assert_eq!(probe.pool_lines, 1024 * MB / 16, "at {bytes} bytes");
assert!(probe.window_lines < probe.pool_lines, "at {bytes} bytes");
assert_eq!(probe.buffer_bytes, 1024 * MB, "at {bytes} bytes");
}
}
#[test]
fn a_small_window_carries_the_passes_that_walk_the_pool() {
let small = probe(8 * KB, MemoryAccess::Read);
assert_eq!(small.min_iterations(), 1024 * MB / (8 * KB));
let whole = probe(512 * MB, MemoryAccess::Read);
assert_eq!(whole.min_iterations(), 2);
}
#[test]
fn walking_the_pool_costs_the_pool_whatever_the_window() {
for bytes in [8 * KB, 256 * KB, 4 * MB, 512 * MB] {
let probe = probe(bytes, MemoryAccess::Read);
let walked = probe.min_iterations() * probe.window_lines;
assert_eq!(walked, probe.pool_lines, "at {bytes} bytes");
}
}
#[test]
fn the_window_stops_at_the_default_working_set() {
let read = probe(512 * MB, MemoryAccess::Read);
assert_eq!(read.window_lines, 512 * MB / 16);
let huge = probe(8 * 1024 * MB, MemoryAccess::Read);
assert_eq!(huge.window_lines, read.window_lines);
}
#[test]
fn the_launch_shrinks_with_the_window() {
assert_eq!(probe(256 * KB, MemoryAccess::Read).cube_count, 64);
assert_eq!(probe(512 * MB, MemoryAccess::Read).cube_count, 2048);
assert_eq!(probe(64, MemoryAccess::Read).cube_count, 1);
}
#[test]
fn a_device_that_allocates_little_shrinks_the_window_with_it() {
let shape = DeviceShape {
max_alloc: 4 * MB,
..DEVICE
};
let spec = MemorySpec::new(MemoryAccess::Read, 512 * MB as u64);
let probe = MemoryProbe::sized(shape, 16, spec, false);
assert_eq!(probe.buffer_bytes, 4 * MB);
assert_eq!(probe.window_lines, 2 * MB / 16);
}
}