pub const MAX_WORKGROUPS_PER_DIM: u32 = 65_535;
pub const COPY_BYTES_PER_ROW_ALIGNMENT: u32 = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
pub max_workgroup_size_x: u32,
pub max_workgroup_size_y: u32,
pub max_workgroup_size_z: u32,
pub max_invocations_per_workgroup: u32,
pub max_workgroups_per_dim: u32,
}
impl Limits {
#[must_use]
pub fn portable_default() -> Self {
Self {
max_workgroup_size_x: 256,
max_workgroup_size_y: 256,
max_workgroup_size_z: 64,
max_invocations_per_workgroup: 256,
max_workgroups_per_dim: MAX_WORKGROUPS_PER_DIM,
}
}
#[must_use]
pub fn desktop_default() -> Self {
Self {
max_workgroup_size_x: 1024,
max_workgroup_size_y: 1024,
max_workgroup_size_z: 64,
max_invocations_per_workgroup: 1024,
max_workgroups_per_dim: MAX_WORKGROUPS_PER_DIM,
}
}
}
#[inline]
#[must_use]
pub fn div_ceil_u32(a: u32, b: u32) -> u32 {
match a.checked_div(b) {
Some(q) => q + u32::from(a % b != 0),
None => 0,
}
}
#[inline]
#[must_use]
pub fn align_up_u32(value: u32, align: u32) -> u32 {
if align == 0 {
return value;
}
div_ceil_u32(value, align).saturating_mul(align)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Workgroup1D {
pub x: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Workgroup2D {
pub x: u32,
pub y: u32,
}
impl Workgroup2D {
#[must_use]
pub fn total(self) -> u32 {
self.x.saturating_mul(self.y)
}
}
#[must_use]
pub fn plan_workgroup_1d(limits: &Limits, preferred: u32) -> Workgroup1D {
let ceiling = limits
.max_workgroup_size_x
.min(limits.max_invocations_per_workgroup);
let want = preferred.max(1).min(ceiling.max(1));
Workgroup1D {
x: floor_pow2(want).max(1),
}
}
#[must_use]
pub fn plan_workgroup_square(limits: &Limits, preferred_tile: u32) -> Workgroup2D {
let axis_cap = limits.max_workgroup_size_x.min(limits.max_workgroup_size_y);
let mut t = floor_pow2(preferred_tile.max(1)).max(1);
t = t.min(floor_pow2(axis_cap.max(1)).max(1));
while t > 1 && t.saturating_mul(t) > limits.max_invocations_per_workgroup {
t >>= 1;
}
Workgroup2D { x: t, y: t }
}
#[inline]
#[must_use]
pub fn floor_pow2(v: u32) -> u32 {
if v == 0 {
0
} else {
1u32 << (31 - v.leading_zeros())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DispatchGrid {
pub x: u32,
pub y: u32,
pub z: u32,
}
impl DispatchGrid {
#[must_use]
pub fn total(self) -> u64 {
u64::from(self.x) * u64::from(self.y) * u64::from(self.z)
}
}
#[allow(clippy::missing_errors_doc)]
pub fn plan_dispatch_1d(
limits: &Limits,
total_threads: u64,
workgroup_x: u32,
) -> Result<(DispatchGrid, u32), String> {
let wg = workgroup_x.max(1);
let groups = div_ceil_u64(total_threads, u64::from(wg));
let max_dim = u64::from(limits.max_workgroups_per_dim.max(1));
if groups <= max_dim {
let gx = u32::try_from(groups).unwrap_or(limits.max_workgroups_per_dim);
return Ok((DispatchGrid { x: gx, y: 1, z: 1 }, gx));
}
let grid_x = max_dim;
let grid_y = div_ceil_u64(groups, grid_x);
if grid_y > max_dim {
return Err(format!(
"dispatch of {groups} workgroups exceeds 2-D capacity ({max_dim} x {max_dim})"
));
}
let gx = u32::try_from(grid_x).unwrap_or(limits.max_workgroups_per_dim);
let gy = u32::try_from(grid_y).unwrap_or(limits.max_workgroups_per_dim);
Ok((DispatchGrid { x: gx, y: gy, z: 1 }, gx))
}
#[allow(clippy::missing_errors_doc)]
pub fn plan_dispatch_2d(
limits: &Limits,
rows_m: u32,
cols_n: u32,
tile: Workgroup2D,
batch: u32,
) -> Result<DispatchGrid, String> {
let gx = div_ceil_u32(cols_n, tile.x.max(1));
let gy = div_ceil_u32(rows_m, tile.y.max(1));
let gz = batch.max(1);
let cap = limits.max_workgroups_per_dim.max(1);
if gx > cap || gy > cap || gz > cap {
return Err(format!(
"2-D dispatch ({gx}, {gy}, {gz}) exceeds per-axis cap {cap}"
));
}
Ok(DispatchGrid {
x: gx,
y: gy,
z: gz,
})
}
#[inline]
#[must_use]
pub fn div_ceil_u64(a: u64, b: u64) -> u64 {
match a.checked_div(b) {
Some(q) => q + u64::from(a % b != 0),
None => 0,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextureRowLayout {
pub unpadded_bytes_per_row: u32,
pub padded_bytes_per_row: u32,
pub total_bytes: u64,
}
#[must_use]
pub fn texture_row_layout(width: u32, height: u32, bytes_per_texel: u32) -> TextureRowLayout {
let unpadded = width.saturating_mul(bytes_per_texel);
let padded = align_up_u32(unpadded, COPY_BYTES_PER_ROW_ALIGNMENT);
TextureRowLayout {
unpadded_bytes_per_row: unpadded,
padded_bytes_per_row: padded,
total_bytes: u64::from(padded) * u64::from(height),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn div_ceil_u32_basic() {
assert_eq!(div_ceil_u32(0, 4), 0);
assert_eq!(div_ceil_u32(1, 4), 1);
assert_eq!(div_ceil_u32(4, 4), 1);
assert_eq!(div_ceil_u32(5, 4), 2);
assert_eq!(div_ceil_u32(8, 4), 2);
assert_eq!(div_ceil_u32(10, 0), 0);
}
#[test]
fn align_up_u32_to_256() {
assert_eq!(align_up_u32(0, 256), 0);
assert_eq!(align_up_u32(1, 256), 256);
assert_eq!(align_up_u32(256, 256), 256);
assert_eq!(align_up_u32(257, 256), 512);
assert_eq!(align_up_u32(513, 256), 768);
assert_eq!(align_up_u32(123, 0), 123);
}
#[test]
fn floor_pow2_values() {
assert_eq!(floor_pow2(0), 0);
assert_eq!(floor_pow2(1), 1);
assert_eq!(floor_pow2(2), 2);
assert_eq!(floor_pow2(3), 2);
assert_eq!(floor_pow2(255), 128);
assert_eq!(floor_pow2(256), 256);
assert_eq!(floor_pow2(257), 256);
assert_eq!(floor_pow2(1024), 1024);
}
#[test]
fn plan_workgroup_1d_respects_invocation_cap() {
let lim = Limits::portable_default(); assert_eq!(plan_workgroup_1d(&lim, 256).x, 256);
assert_eq!(plan_workgroup_1d(&lim, 1024).x, 256);
assert_eq!(plan_workgroup_1d(&lim, 200).x, 128);
assert_eq!(plan_workgroup_1d(&lim, 0).x, 1);
}
#[test]
fn plan_workgroup_1d_desktop_allows_larger() {
let lim = Limits::desktop_default(); assert_eq!(plan_workgroup_1d(&lim, 1024).x, 1024);
assert_eq!(plan_workgroup_1d(&lim, 2048).x, 1024);
}
#[test]
fn plan_workgroup_square_caps_at_16_on_portable() {
let lim = Limits::portable_default(); let wg = plan_workgroup_square(&lim, 32);
assert_eq!(wg, Workgroup2D { x: 16, y: 16 });
assert!(wg.total() <= lim.max_invocations_per_workgroup);
}
#[test]
fn plan_workgroup_square_allows_32_on_desktop() {
let lim = Limits::desktop_default(); let wg = plan_workgroup_square(&lim, 32);
assert_eq!(wg, Workgroup2D { x: 32, y: 32 });
assert_eq!(wg.total(), 1024);
}
#[test]
fn plan_workgroup_square_respects_preferred_smaller() {
let lim = Limits::desktop_default();
let wg = plan_workgroup_square(&lim, 8);
assert_eq!(wg, Workgroup2D { x: 8, y: 8 });
}
#[test]
fn plan_dispatch_1d_simple() {
let lim = Limits::portable_default();
let (grid, grid_x) = plan_dispatch_1d(&lim, 1000, 256).expect("plan");
assert_eq!(grid, DispatchGrid { x: 4, y: 1, z: 1 });
assert_eq!(grid_x, 4);
}
#[test]
fn plan_dispatch_1d_exact_multiple() {
let lim = Limits::portable_default();
let (grid, _) = plan_dispatch_1d(&lim, 512, 256).expect("plan");
assert_eq!(grid.x, 2);
assert_eq!(grid.total(), 2);
}
#[test]
fn plan_dispatch_1d_folds_into_2d_when_too_wide() {
let lim = Limits::portable_default(); let total_groups = u64::from(MAX_WORKGROUPS_PER_DIM) * 2 + 1;
let total_threads = total_groups; let (grid, grid_x) = plan_dispatch_1d(&lim, total_threads, 1).expect("plan");
assert!(grid.y > 1, "expected 2-D fold, got {grid:?}");
assert_eq!(grid.x, MAX_WORKGROUPS_PER_DIM);
assert_eq!(grid_x, MAX_WORKGROUPS_PER_DIM);
assert!(grid.total() >= total_groups);
assert!(u64::from(grid.x) * u64::from(grid.y) >= total_groups);
}
#[test]
fn plan_dispatch_1d_errors_when_beyond_2d_capacity() {
let lim = Limits::portable_default();
let beyond = u64::from(MAX_WORKGROUPS_PER_DIM) * u64::from(MAX_WORKGROUPS_PER_DIM) + 1;
let err = plan_dispatch_1d(&lim, beyond, 1).unwrap_err();
assert!(err.contains("exceeds 2-D capacity"));
}
#[test]
fn plan_dispatch_2d_gemm() {
let lim = Limits::portable_default();
let tile = Workgroup2D { x: 16, y: 16 };
let grid = plan_dispatch_2d(&lim, 100, 200, tile, 1).expect("plan");
assert_eq!(grid.x, div_ceil_u32(200, 16)); assert_eq!(grid.y, div_ceil_u32(100, 16)); assert_eq!(grid.z, 1);
}
#[test]
fn plan_dispatch_2d_batched() {
let lim = Limits::portable_default();
let tile = Workgroup2D { x: 8, y: 8 };
let grid = plan_dispatch_2d(&lim, 32, 32, tile, 7).expect("plan");
assert_eq!(grid.z, 7);
assert_eq!(grid.x, 4);
assert_eq!(grid.y, 4);
}
#[test]
fn plan_dispatch_2d_errors_when_axis_overflows() {
let lim = Limits::portable_default();
let tile = Workgroup2D { x: 1, y: 1 };
let huge = MAX_WORKGROUPS_PER_DIM + 10;
let err = plan_dispatch_2d(&lim, 4, huge, tile, 1).unwrap_err();
assert!(err.contains("exceeds per-axis cap"));
}
#[test]
fn texture_row_layout_padding() {
let layout = texture_row_layout(100, 50, 4);
assert_eq!(layout.unpadded_bytes_per_row, 400);
assert_eq!(layout.padded_bytes_per_row, 512);
assert_eq!(layout.total_bytes, 512 * 50);
assert_eq!(
layout.padded_bytes_per_row % COPY_BYTES_PER_ROW_ALIGNMENT,
0
);
}
#[test]
fn texture_row_layout_already_aligned() {
let layout = texture_row_layout(64, 10, 4);
assert_eq!(layout.unpadded_bytes_per_row, 256);
assert_eq!(layout.padded_bytes_per_row, 256);
assert_eq!(layout.total_bytes, 2560);
}
#[test]
fn limits_portable_default_is_conservative() {
let lim = Limits::portable_default();
assert_eq!(lim.max_invocations_per_workgroup, 256);
assert_eq!(lim.max_workgroups_per_dim, MAX_WORKGROUPS_PER_DIM);
let desk = Limits::desktop_default();
assert!(desk.max_invocations_per_workgroup >= lim.max_invocations_per_workgroup);
}
}