use cubecl::prelude::*;
use crate::errors::CubeclUtilsErrors;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GpuLimits {
pub max_shared_bytes: usize,
pub max_cube_count: (u32, u32, u32),
pub max_units_per_cube: u32,
pub max_cube_dim: (u32, u32, u32),
pub max_binding_bytes: u64,
pub plane_size_min: u32,
pub plane_size_max: u32,
}
impl GpuLimits {
pub fn from_client<R: Runtime>(client: &ComputeClient<R>) -> Self {
let props = client.properties();
let hw = &props.hardware;
Self {
max_shared_bytes: hw.max_shared_memory_size,
max_cube_count: hw.max_cube_count,
max_units_per_cube: hw.max_units_per_cube,
max_cube_dim: hw.max_cube_dim,
max_binding_bytes: props.memory.max_page_size,
plane_size_min: hw.plane_size_min,
plane_size_max: hw.plane_size_max,
}
}
}
pub fn grid_2d_limited(total_cubes: u32, max_dim: u32) -> Result<(u32, u32), CubeclUtilsErrors> {
let total = total_cubes.max(1);
let limit = max_dim.max(1);
let x = total.min(limit);
let y = total.div_ceil(x);
if y > limit {
return Err(CubeclUtilsErrors::GridTooLarge {
total_cubes: total,
max_dim: limit,
});
}
Ok((x, y))
}
pub fn grid_2d(total_cubes: u32, limits: &GpuLimits) -> Result<(u32, u32), CubeclUtilsErrors> {
let (mx, my, _) = limits.max_cube_count;
grid_2d_limited(total_cubes, mx.min(my))
}
pub fn checked_cube_count(
kernel: &'static str,
x: u32,
y: u32,
z: u32,
limits: &GpuLimits,
) -> Result<CubeCount, CubeclUtilsErrors> {
let limit = limits.max_cube_count;
if x > limit.0 || y > limit.1 || z > limit.2 {
return Err(CubeclUtilsErrors::CubeCountExceeded {
kernel,
requested: (x, y, z),
limit,
});
}
Ok(CubeCount::Static(x, y, z))
}
pub fn resolve_workgroup_size(preferred: u32, limits: &GpuLimits) -> u32 {
let cap = limits.max_units_per_cube.min(limits.max_cube_dim.0).max(1);
let wanted = preferred.clamp(1, cap);
let plane = limits.plane_size_max.max(1);
if plane > wanted {
return wanted;
}
(wanted / plane) * plane
}
pub fn plane_uniform(wg_size: u32, limits: &GpuLimits) -> bool {
limits.plane_size_min == wg_size && limits.plane_size_max == wg_size
}
pub fn plane_partitions(wg_size: u32, limits: &GpuLimits) -> Option<u32> {
let plane = limits.plane_size_min;
if plane == 0 || plane != limits.plane_size_max || wg_size == 0 {
return None;
}
if !wg_size.is_multiple_of(plane) {
return None;
}
Some(wg_size / plane)
}
pub fn fits_binding(requested: u64, limits: &GpuLimits) -> Result<(), CubeclUtilsErrors> {
if requested > limits.max_binding_bytes {
return Err(CubeclUtilsErrors::BindingTooLarge {
requested,
limit: limits.max_binding_bytes,
});
}
Ok(())
}
pub fn fits_shared_memory(
kernel: &'static str,
requested: usize,
limits: &GpuLimits,
) -> Result<(), CubeclUtilsErrors> {
if requested > limits.max_shared_bytes {
return Err(CubeclUtilsErrors::SharedMemoryExceeded {
kernel,
requested,
available: limits.max_shared_bytes,
});
}
Ok(())
}
pub fn resident_workgroups(footprint_bytes: usize, limits: &GpuLimits) -> usize {
if footprint_bytes == 0 {
return usize::MAX;
}
limits.max_shared_bytes / footprint_bytes
}
#[cfg(test)]
mod tests {
use super::*;
fn apple() -> GpuLimits {
GpuLimits {
max_shared_bytes: 32_768,
max_cube_count: (65_535, 65_535, 65_535),
max_units_per_cube: 1024,
max_cube_dim: (1024, 1024, 1024),
max_binding_bytes: 4_294_967_292,
plane_size_min: 32,
plane_size_max: 32,
}
}
fn small() -> GpuLimits {
GpuLimits {
max_shared_bytes: 16_384,
max_cube_count: (32_768, 1_024, 64),
max_units_per_cube: 256,
max_cube_dim: (256, 256, 64),
max_binding_bytes: 128 * 1024 * 1024,
plane_size_min: 64,
plane_size_max: 64,
}
}
fn legacy_grid_2d(total_cubes: u32) -> (u32, u32) {
let x = total_cubes.min(65535);
let y = total_cubes.div_ceil(x);
(x, y)
}
#[test]
fn test_grid_2d_packing_matches_legacy() {
for total in [
1u32,
2,
31,
32,
1000,
65_534,
65_535,
65_536,
65_537,
131_070,
131_071,
1_000_000,
10_000_000,
100_000_000,
] {
assert_eq!(
grid_2d_limited(total, 65_535).unwrap(),
legacy_grid_2d(total),
"packing drifted at total = {total}"
);
}
}
#[test]
fn test_grid_2d_zero_does_not_panic() {
assert_eq!(grid_2d_limited(0, 65_535).unwrap(), (1, 1));
}
#[test]
fn test_grid_2d_covers_and_fits() {
for max_dim in [65_535u32, 32_768, 1024] {
for total in [0u32, 1, 65_535, 65_536, 131_070, 10_000_000] {
let capacity = max_dim as u64 * max_dim as u64;
match grid_2d_limited(total, max_dim) {
Ok((x, y)) => {
assert!(
x <= max_dim && y <= max_dim,
"over limit at {total}/{max_dim}"
);
assert!(
x as u64 * y as u64 >= total.max(1) as u64,
"uncovered at {total}/{max_dim}"
);
}
Err(_) => assert!(
total as u64 > capacity,
"refused a grid that fits at {total}/{max_dim}"
),
}
}
}
}
#[test]
fn test_grid_2d_errors_past_max_dim_squared() {
assert!(grid_2d_limited(1024 * 1024, 1024).is_ok());
assert!(matches!(
grid_2d_limited(1024 * 1024 + 1, 1024),
Err(CubeclUtilsErrors::GridTooLarge { .. })
));
}
#[test]
fn test_grid_2d_uses_smaller_of_x_and_y() {
let (x, y) = grid_2d(4096, &small()).unwrap();
assert!(x <= 1024 && y <= 1024, "got {x}x{y}");
assert!(x as u64 * y as u64 >= 4096);
}
#[test]
fn test_checked_cube_count_accepts_at_the_limit() {
assert!(checked_cube_count("k", 65_535, 65_535, 65_535, &apple()).is_ok());
}
#[test]
fn test_checked_cube_count_errors_per_axis() {
let l = small();
for (x, y, z) in [(32_769, 1, 1), (1, 32_769, 1), (1, 1, 65)] {
assert!(
matches!(
checked_cube_count("k", x, y, z, &l),
Err(CubeclUtilsErrors::CubeCountExceeded { .. })
),
"accepted {x},{y},{z}"
);
}
}
#[test]
fn test_resolve_workgroup_size_apple() {
assert_eq!(resolve_workgroup_size(256, &apple()), 256);
}
#[test]
fn test_resolve_workgroup_size_caps_and_rounds() {
assert_eq!(resolve_workgroup_size(512, &small()), 256);
assert_eq!(resolve_workgroup_size(200, &small()), 192);
}
#[test]
fn test_resolve_workgroup_size_is_whole_planes_and_nonzero() {
for plane in [8u32, 16, 32, 64] {
for cap in [256u32, 512, 1024] {
let l = GpuLimits {
max_units_per_cube: cap,
max_cube_dim: (cap, cap, cap),
plane_size_min: plane,
plane_size_max: plane,
..apple()
};
for preferred in [1u32, 32, 100, 256, 4096] {
let wg = resolve_workgroup_size(preferred, &l);
assert!(wg > 0, "zero width at plane {plane}, cap {cap}");
assert!(wg <= cap, "over cap at plane {plane}, cap {cap}");
if preferred >= plane {
assert_eq!(wg % plane, 0, "partial plane at {plane}/{cap}/{preferred}");
}
}
}
}
}
#[test]
fn test_resolve_workgroup_size_plane_wider_than_cap() {
let l = GpuLimits {
max_units_per_cube: 64,
max_cube_dim: (64, 64, 64),
plane_size_min: 8,
plane_size_max: 128,
..apple()
};
assert_eq!(resolve_workgroup_size(256, &l), 64);
}
#[test]
fn test_plane_uniform() {
assert!(plane_uniform(32, &apple()));
assert!(!plane_uniform(64, &apple()));
assert!(plane_uniform(64, &small()));
}
#[test]
fn test_plane_uniform_false_on_a_range() {
let l = GpuLimits {
plane_size_min: 8,
plane_size_max: 32,
..apple()
};
assert!(
!plane_uniform(32, &l),
"a reported range is not a guarantee"
);
}
#[test]
fn test_plane_partitions() {
assert_eq!(plane_partitions(256, &apple()), Some(8));
assert_eq!(plane_partitions(256, &small()), Some(4));
assert_eq!(plane_partitions(96, &small()), None);
let ranged = GpuLimits {
plane_size_min: 8,
plane_size_max: 32,
..apple()
};
assert_eq!(plane_partitions(256, &ranged), None);
}
#[test]
fn test_fits_binding_boundary() {
let l = small();
let limit = 128 * 1024 * 1024;
assert!(fits_binding(limit, &l).is_ok());
assert!(matches!(
fits_binding(limit + 1, &l),
Err(CubeclUtilsErrors::BindingTooLarge { .. })
));
}
#[test]
fn test_fits_binding_across_element_sizes() {
let elems: u64 = 8192 * 16_384;
for (bytes_per_elem, name) in [(4u64, "f32/u32"), (8, "f64")] {
let bytes = elems * bytes_per_elem;
assert!(fits_binding(bytes, &apple()).is_ok(), "{name} on apple");
assert!(fits_binding(bytes, &small()).is_err(), "{name} on small");
}
}
#[test]
fn test_fits_shared_memory_boundary() {
let l = apple();
assert!(fits_shared_memory("k", 32_768, &l).is_ok());
assert!(matches!(
fits_shared_memory("k", 32_769, &l),
Err(CubeclUtilsErrors::SharedMemoryExceeded { .. })
));
}
#[test]
fn test_fits_shared_memory_apple_budget_busts_a_small_device() {
assert!(fits_shared_memory("k", 20_000, &apple()).is_ok());
assert!(fits_shared_memory("k", 20_000, &small()).is_err());
}
#[test]
fn test_resident_workgroups() {
let l = apple();
assert_eq!(resident_workgroups(22_024, &l), 1);
assert_eq!(resident_workgroups(13_320, &l), 2);
assert_eq!(resident_workgroups(8_968, &l), 3);
assert_eq!(resident_workgroups(40_000, &l), 0);
}
}