#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KernelAttributes {
pub max_threads_per_block: u32,
pub num_regs: u32,
pub static_shared_bytes: u32,
pub local_bytes: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeviceLimits {
pub max_threads_per_block: u32,
pub max_shared_per_block: u32,
pub max_regs_per_block: u32,
pub warp_size: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum LaunchBudgetViolation {
#[error("block size {requested} exceeds the kernel's max_threads_per_block {kernel_max} (register pressure); launch would fail with CUDA_ERROR_INVALID_VALUE")]
BlockExceedsKernelMax {
requested: u32,
kernel_max: u32,
},
#[error("block size {requested} exceeds the device's max_threads_per_block {device_max}")]
BlockExceedsDeviceMax {
requested: u32,
device_max: u32,
},
#[error("shared memory {static_bytes}+{dynamic_bytes}={total} bytes exceeds the device's max_shared_per_block {device_max}")]
SharedMemoryExceeded {
static_bytes: u32,
dynamic_bytes: u32,
total: u32,
device_max: u32,
},
#[error("register budget {num_regs}regs x {block_size}threads = {total} exceeds the device's max_regs_per_block {device_max}")]
RegisterBudgetExceeded {
num_regs: u32,
block_size: u32,
total: u32,
device_max: u32,
},
#[error("block size is zero")]
ZeroBlockSize,
}
pub fn validate_launch(
attrs: &KernelAttributes,
limits: &DeviceLimits,
block_size: u32,
dynamic_shared_bytes: u32,
) -> Result<(), LaunchBudgetViolation> {
if block_size == 0 {
return Err(LaunchBudgetViolation::ZeroBlockSize);
}
if block_size > attrs.max_threads_per_block {
return Err(LaunchBudgetViolation::BlockExceedsKernelMax {
requested: block_size,
kernel_max: attrs.max_threads_per_block,
});
}
if block_size > limits.max_threads_per_block {
return Err(LaunchBudgetViolation::BlockExceedsDeviceMax {
requested: block_size,
device_max: limits.max_threads_per_block,
});
}
let total_shared = attrs
.static_shared_bytes
.saturating_add(dynamic_shared_bytes);
if total_shared > limits.max_shared_per_block {
return Err(LaunchBudgetViolation::SharedMemoryExceeded {
static_bytes: attrs.static_shared_bytes,
dynamic_bytes: dynamic_shared_bytes,
total: total_shared,
device_max: limits.max_shared_per_block,
});
}
let total_regs = attrs.num_regs.saturating_mul(block_size);
if total_regs > limits.max_regs_per_block {
return Err(LaunchBudgetViolation::RegisterBudgetExceeded {
num_regs: attrs.num_regs,
block_size,
total: total_regs,
device_max: limits.max_regs_per_block,
});
}
Ok(())
}
#[must_use]
pub fn spills_to_local(attrs: &KernelAttributes) -> bool {
attrs.local_bytes > 0
}
#[cfg(test)]
mod tests {
use super::*;
fn ok_kernel() -> KernelAttributes {
KernelAttributes {
max_threads_per_block: 1024,
num_regs: 10,
static_shared_bytes: 0,
local_bytes: 0,
}
}
fn limits() -> DeviceLimits {
DeviceLimits {
max_threads_per_block: 1024,
max_shared_per_block: 49152,
max_regs_per_block: 65536,
warp_size: 32,
}
}
#[test]
fn accepts_a_legal_launch() {
assert!(validate_launch(&ok_kernel(), &limits(), 256, 0).is_ok());
}
#[test]
fn accepts_the_kernel_max_exactly() {
assert!(validate_launch(&ok_kernel(), &limits(), 1024, 0).is_ok());
}
#[test]
fn rejects_zero_block_size() {
assert_eq!(
validate_launch(&ok_kernel(), &limits(), 0, 0),
Err(LaunchBudgetViolation::ZeroBlockSize)
);
}
#[test]
fn rejects_block_above_kernel_max() {
let heavy = KernelAttributes {
max_threads_per_block: 128,
num_regs: 200,
static_shared_bytes: 0,
local_bytes: 0,
};
assert_eq!(
validate_launch(&heavy, &limits(), 256, 0),
Err(LaunchBudgetViolation::BlockExceedsKernelMax {
requested: 256,
kernel_max: 128,
})
);
}
#[test]
fn kernel_max_is_reported_before_device_max() {
let heavy = KernelAttributes {
max_threads_per_block: 128,
..ok_kernel()
};
let tight = DeviceLimits {
max_threads_per_block: 512,
..limits()
};
assert!(matches!(
validate_launch(&heavy, &tight, 2048, 0),
Err(LaunchBudgetViolation::BlockExceedsKernelMax { .. })
));
}
#[test]
fn rejects_block_above_device_max() {
let permissive = KernelAttributes {
max_threads_per_block: 4096,
..ok_kernel()
};
assert_eq!(
validate_launch(&permissive, &limits(), 2048, 0),
Err(LaunchBudgetViolation::BlockExceedsDeviceMax {
requested: 2048,
device_max: 1024,
})
);
}
#[test]
fn rejects_shared_memory_overflow_counting_both_halves() {
let k = KernelAttributes {
static_shared_bytes: 32768,
..ok_kernel()
};
assert_eq!(
validate_launch(&k, &limits(), 256, 32768),
Err(LaunchBudgetViolation::SharedMemoryExceeded {
static_bytes: 32768,
dynamic_bytes: 32768,
total: 65536,
device_max: 49152,
})
);
}
#[test]
fn accepts_shared_memory_exactly_at_the_limit() {
let k = KernelAttributes {
static_shared_bytes: 49152,
..ok_kernel()
};
assert!(validate_launch(&k, &limits(), 256, 0).is_ok());
}
#[test]
fn rejects_register_budget_overflow() {
let k = KernelAttributes {
max_threads_per_block: 1024,
num_regs: 255,
..ok_kernel()
};
assert_eq!(
validate_launch(&k, &limits(), 1024, 0),
Err(LaunchBudgetViolation::RegisterBudgetExceeded {
num_regs: 255,
block_size: 1024,
total: 261_120,
device_max: 65536,
})
);
}
#[test]
fn register_product_saturates_instead_of_overflowing() {
let k = KernelAttributes {
max_threads_per_block: u32::MAX,
num_regs: u32::MAX,
..ok_kernel()
};
let wide = DeviceLimits {
max_threads_per_block: u32::MAX,
..limits()
};
assert!(matches!(
validate_launch(&k, &wide, u32::MAX, 0),
Err(LaunchBudgetViolation::RegisterBudgetExceeded { .. })
));
}
#[test]
fn shared_memory_sum_saturates_instead_of_overflowing() {
let k = KernelAttributes {
static_shared_bytes: u32::MAX,
..ok_kernel()
};
assert!(matches!(
validate_launch(&k, &limits(), 256, u32::MAX),
Err(LaunchBudgetViolation::SharedMemoryExceeded { .. })
));
}
#[test]
fn spill_detection_is_separate_from_validity() {
let spilled = KernelAttributes {
local_bytes: 128,
..ok_kernel()
};
assert!(validate_launch(&spilled, &limits(), 256, 0).is_ok());
assert!(spills_to_local(&spilled));
assert!(!spills_to_local(&ok_kernel()));
}
#[test]
fn violations_name_both_sides_of_the_comparison() {
let heavy = KernelAttributes {
max_threads_per_block: 128,
..ok_kernel()
};
let Err(v) = validate_launch(&heavy, &limits(), 256, 0) else {
panic!("expected a violation");
};
let msg = v.to_string();
assert!(msg.contains("256"), "message must name the request: {msg}");
assert!(msg.contains("128"), "message must name the limit: {msg}");
}
}