use crate::GpuOptimError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SmResourceLimits {
pub compute_capability: (u32, u32),
pub warp_size: u32,
pub max_threads_per_block: u32,
pub max_threads_per_sm: u32,
pub max_warps_per_sm: u32,
pub max_blocks_per_sm: u32,
pub registers_per_sm: u32,
pub register_alloc_granularity: u32,
pub shared_mem_per_sm_bytes: usize,
}
impl SmResourceLimits {
#[must_use]
pub const fn sm_70() -> Self {
Self {
compute_capability: (7, 0),
warp_size: 32,
max_threads_per_block: 1024,
max_threads_per_sm: 2048,
max_warps_per_sm: 64,
max_blocks_per_sm: 32,
registers_per_sm: 65536,
register_alloc_granularity: 256,
shared_mem_per_sm_bytes: 98_304,
}
}
#[must_use]
pub const fn sm_75() -> Self {
Self {
compute_capability: (7, 5),
warp_size: 32,
max_threads_per_block: 1024,
max_threads_per_sm: 1024,
max_warps_per_sm: 32,
max_blocks_per_sm: 16,
registers_per_sm: 65536,
register_alloc_granularity: 256,
shared_mem_per_sm_bytes: 65_536,
}
}
#[must_use]
pub const fn sm_80() -> Self {
Self {
compute_capability: (8, 0),
warp_size: 32,
max_threads_per_block: 1024,
max_threads_per_sm: 2048,
max_warps_per_sm: 64,
max_blocks_per_sm: 32,
registers_per_sm: 65536,
register_alloc_granularity: 256,
shared_mem_per_sm_bytes: 167_936,
}
}
#[must_use]
pub const fn sm_86() -> Self {
Self {
compute_capability: (8, 6),
warp_size: 32,
max_threads_per_block: 1024,
max_threads_per_sm: 1536,
max_warps_per_sm: 48,
max_blocks_per_sm: 16,
registers_per_sm: 65536,
register_alloc_granularity: 256,
shared_mem_per_sm_bytes: 102_400,
}
}
#[must_use]
pub const fn sm_90() -> Self {
Self {
compute_capability: (9, 0),
warp_size: 32,
max_threads_per_block: 1024,
max_threads_per_sm: 2048,
max_warps_per_sm: 64,
max_blocks_per_sm: 32,
registers_per_sm: 65536,
register_alloc_granularity: 256,
shared_mem_per_sm_bytes: 233_472,
}
}
pub fn from_compute_capability(compute_capability: (u32, u32)) -> Result<Self, GpuOptimError> {
let (major, minor) = compute_capability;
match (major, minor) {
(7, 0) | (7, 2) => Ok(Self::sm_70()),
(7, 5) => Ok(Self::sm_75()),
(8, 0) => Ok(Self::sm_80()),
(8, 6) | (8, 7) | (8, 9) => Ok(Self::sm_86()),
(9, 0) => Ok(Self::sm_90()),
_ => Err(GpuOptimError::UnsupportedOperation(format!(
"no CUDA occupancy model for compute capability {major}.{minor}"
))),
}
}
pub fn from_device_capabilities(
capabilities: &crate::backends::DeviceCapabilities,
) -> Result<Self, GpuOptimError> {
let mut limits = Self::from_compute_capability(capabilities.compute_capability)?;
if capabilities.max_threads_per_block > 0 {
limits.max_threads_per_block = capabilities.max_threads_per_block;
}
Ok(limits)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KernelResourceUsage {
pub registers_per_thread: u32,
pub shared_mem_per_block_bytes: usize,
pub threads_per_block: u32,
}
impl KernelResourceUsage {
#[must_use]
pub const fn new(
registers_per_thread: u32,
shared_mem_per_block_bytes: usize,
threads_per_block: u32,
) -> Self {
Self {
registers_per_thread,
shared_mem_per_block_bytes,
threads_per_block,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OccupancyLimiter {
Warps,
Registers,
SharedMemory,
BlocksPerSm,
ThreadsPerBlock,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OccupancyResult {
pub active_warps_per_sm: u32,
pub active_blocks_per_sm: u32,
pub max_warps_per_sm: u32,
pub occupancy: f64,
pub limiter: OccupancyLimiter,
}
const fn round_up_to_multiple(value: u64, granularity: u64) -> u64 {
if granularity == 0 {
return value;
}
match value.div_ceil(granularity).checked_mul(granularity) {
Some(rounded) => rounded,
None => u64::MAX,
}
}
const fn clamp_u64_to_u32(value: u64) -> u32 {
if value > u32::MAX as u64 {
u32::MAX
} else {
value as u32
}
}
const fn clamp_usize_to_u32(value: usize) -> u32 {
if value > u32::MAX as usize {
u32::MAX
} else {
value as u32
}
}
pub fn calculate_occupancy(
usage: &KernelResourceUsage,
limits: &SmResourceLimits,
) -> Result<OccupancyResult, GpuOptimError> {
let warp_size = limits.warp_size;
let threads_per_block = usage.threads_per_block;
if warp_size == 0 {
return Err(GpuOptimError::InvalidState(
"warp_size must be greater than zero".to_string(),
));
}
if threads_per_block == 0 {
return Err(GpuOptimError::InvalidState(
"threads_per_block must be greater than zero".to_string(),
));
}
if threads_per_block > limits.max_threads_per_block {
return Ok(OccupancyResult {
active_warps_per_sm: 0,
active_blocks_per_sm: 0,
max_warps_per_sm: limits.max_warps_per_sm,
occupancy: 0.0,
limiter: OccupancyLimiter::ThreadsPerBlock,
});
}
let warps_per_block = threads_per_block.div_ceil(warp_size);
let warps_limit = limits.max_warps_per_sm / warps_per_block;
let register_limit = if usage.registers_per_thread == 0 {
u32::MAX
} else {
let raw_registers =
u64::from(usage.registers_per_thread).saturating_mul(u64::from(threads_per_block));
let registers_per_block =
round_up_to_multiple(raw_registers, u64::from(limits.register_alloc_granularity))
.max(1);
clamp_u64_to_u32(u64::from(limits.registers_per_sm) / registers_per_block)
};
let shared_mem_limit = match limits
.shared_mem_per_sm_bytes
.checked_div(usage.shared_mem_per_block_bytes)
{
Some(blocks) => clamp_usize_to_u32(blocks),
None => u32::MAX,
};
let block_cap_limit = limits.max_blocks_per_sm;
let candidates = [
(warps_limit, OccupancyLimiter::Warps),
(register_limit, OccupancyLimiter::Registers),
(shared_mem_limit, OccupancyLimiter::SharedMemory),
(block_cap_limit, OccupancyLimiter::BlocksPerSm),
];
let mut active_blocks = candidates[0].0;
let mut limiter = candidates[0].1;
for &(value, candidate_limiter) in &candidates[1..] {
if value < active_blocks {
active_blocks = value;
limiter = candidate_limiter;
}
}
let active_warps = active_blocks.saturating_mul(warps_per_block);
let occupancy = if limits.max_warps_per_sm == 0 {
0.0
} else {
f64::from(active_warps) / f64::from(limits.max_warps_per_sm)
};
Ok(OccupancyResult {
active_warps_per_sm: active_warps,
active_blocks_per_sm: active_blocks,
max_warps_per_sm: limits.max_warps_per_sm,
occupancy,
limiter,
})
}
pub fn optimal_block_size<F>(
registers_per_thread: u32,
shared_mem_per_block: F,
limits: &SmResourceLimits,
) -> Result<(u32, OccupancyResult), GpuOptimError>
where
F: Fn(u32) -> usize,
{
if limits.warp_size == 0 {
return Err(GpuOptimError::InvalidState(
"warp_size must be greater than zero".to_string(),
));
}
let mut best: Option<(u32, OccupancyResult)> = None;
let mut block_size = limits.warp_size;
while block_size <= limits.max_threads_per_block {
let usage = KernelResourceUsage {
registers_per_thread,
shared_mem_per_block_bytes: shared_mem_per_block(block_size),
threads_per_block: block_size,
};
let result = calculate_occupancy(&usage, limits)?;
let replace = match &best {
None => true,
Some((best_block_size, best_result)) => {
result.active_warps_per_sm > best_result.active_warps_per_sm
|| (result.active_warps_per_sm == best_result.active_warps_per_sm
&& block_size > *best_block_size)
}
};
if replace {
best = Some((block_size, result));
}
block_size += limits.warp_size;
}
best.ok_or_else(|| {
GpuOptimError::InvalidState(
"no block size that is a multiple of warp_size fits within max_threads_per_block"
.to_string(),
)
})
}
pub fn occupancy_for_launch(
config: &crate::backends::LaunchConfig,
registers_per_thread: u32,
limits: &SmResourceLimits,
) -> Result<OccupancyResult, GpuOptimError> {
let (block_x, block_y, block_z) = config.block_size;
let threads_per_block = block_x
.checked_mul(block_y)
.and_then(|partial| partial.checked_mul(block_z))
.ok_or_else(|| {
GpuOptimError::InvalidState("block_size dimension product overflows u32".to_string())
})?;
let usage = KernelResourceUsage {
registers_per_thread,
shared_mem_per_block_bytes: config.shared_memory_size,
threads_per_block,
};
calculate_occupancy(&usage, limits)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(actual: f64, expected: f64) -> bool {
(actual - expected).abs() < 1e-9
}
#[test]
fn textbook_sm80_256_threads_32_registers() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(32, 0, 256);
let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
assert_eq!(result.active_blocks_per_sm, 8);
assert_eq!(result.active_warps_per_sm, 64);
assert_eq!(result.max_warps_per_sm, 64);
assert!(approx(result.occupancy, 1.0));
assert_eq!(result.limiter, OccupancyLimiter::Warps);
}
#[test]
fn register_bound_sm80() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(64, 0, 256);
let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
assert_eq!(result.active_blocks_per_sm, 4);
assert_eq!(result.active_warps_per_sm, 32);
assert!(approx(result.occupancy, 0.5));
assert_eq!(result.limiter, OccupancyLimiter::Registers);
}
#[test]
fn shared_memory_bound_sm80() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(16, 48 * 1024, 128);
let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
assert_eq!(result.active_blocks_per_sm, 3);
assert_eq!(result.active_warps_per_sm, 12);
assert!(approx(result.occupancy, 12.0 / 64.0));
assert_eq!(result.limiter, OccupancyLimiter::SharedMemory);
}
#[test]
fn block_cap_dominates_with_tiny_blocks_sm80() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(0, 0, 32);
let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
assert_eq!(result.active_blocks_per_sm, 32);
assert_eq!(result.active_warps_per_sm, 32);
assert!(approx(result.occupancy, 0.5));
assert_eq!(result.limiter, OccupancyLimiter::BlocksPerSm);
}
#[test]
fn register_allocation_granularity_rounds_up() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(33, 0, 96);
let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
assert_eq!(result.active_blocks_per_sm, 19);
assert_eq!(result.active_warps_per_sm, 57);
assert!(approx(result.occupancy, 57.0 / 64.0));
assert_eq!(result.limiter, OccupancyLimiter::Registers);
}
#[test]
fn warps_per_block_uses_ceiling() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(0, 0, 100);
let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
assert_eq!(result.active_blocks_per_sm, 16);
assert_eq!(result.active_warps_per_sm, 64);
assert_eq!(result.limiter, OccupancyLimiter::Warps);
}
#[test]
fn threads_exceeding_hardware_limit_report_threadsperblock() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(32, 0, 2048); let result = calculate_occupancy(&usage, &limits).expect("returns zero-occupancy result");
assert_eq!(result.active_blocks_per_sm, 0);
assert_eq!(result.active_warps_per_sm, 0);
assert!(approx(result.occupancy, 0.0));
assert_eq!(result.limiter, OccupancyLimiter::ThreadsPerBlock);
}
#[test]
fn zero_threads_is_an_error() {
let limits = SmResourceLimits::sm_80();
let usage = KernelResourceUsage::new(32, 0, 0);
assert!(calculate_occupancy(&usage, &limits).is_err());
}
#[test]
fn optimal_block_size_prefers_full_occupancy_and_largest_block() {
let limits = SmResourceLimits::sm_80();
let (block_size, result) =
optimal_block_size(32, |_| 0, &limits).expect("a candidate exists");
assert_eq!(block_size % limits.warp_size, 0);
assert!(block_size <= limits.max_threads_per_block);
assert_eq!(block_size, 1024);
assert!(approx(result.occupancy, 1.0));
let spot = calculate_occupancy(&KernelResourceUsage::new(32, 0, 256), &limits)
.expect("valid configuration");
assert!(result.occupancy >= spot.occupancy);
}
#[test]
fn optimal_block_size_finds_register_heavy_sweet_spot() {
let limits = SmResourceLimits::sm_80();
let (block_size, result) =
optimal_block_size(96, |_| 0, &limits).expect("a candidate exists");
assert_eq!(result.active_warps_per_sm, 21);
assert!(approx(result.occupancy, 21.0 / 64.0));
assert_eq!(result.limiter, OccupancyLimiter::Registers);
let mut probe = limits.warp_size;
while probe <= limits.max_threads_per_block {
let candidate = calculate_occupancy(&KernelResourceUsage::new(96, 0, probe), &limits)
.expect("valid configuration");
assert!(result.active_warps_per_sm >= candidate.active_warps_per_sm);
if candidate.active_warps_per_sm == result.active_warps_per_sm {
assert!(probe <= block_size);
}
probe += limits.warp_size;
}
assert_eq!(block_size, 672);
}
#[test]
fn optimal_block_size_supports_block_dependent_shared_memory() {
let limits = SmResourceLimits::sm_80();
let (block_size, result) = optimal_block_size(16, |threads| threads as usize * 4, &limits)
.expect("a candidate exists");
assert_eq!(block_size % limits.warp_size, 0);
assert!(block_size <= limits.max_threads_per_block);
assert!(result.occupancy > 0.0);
}
#[test]
fn from_compute_capability_maps_known_architectures() {
assert_eq!(
SmResourceLimits::from_compute_capability((7, 0)).expect("known"),
SmResourceLimits::sm_70()
);
assert_eq!(
SmResourceLimits::from_compute_capability((7, 5)).expect("known"),
SmResourceLimits::sm_75()
);
assert_eq!(
SmResourceLimits::from_compute_capability((8, 0)).expect("known"),
SmResourceLimits::sm_80()
);
assert_eq!(
SmResourceLimits::from_compute_capability((8, 6)).expect("known"),
SmResourceLimits::sm_86()
);
assert_eq!(
SmResourceLimits::from_compute_capability((9, 0)).expect("known"),
SmResourceLimits::sm_90()
);
assert_eq!(
SmResourceLimits::from_compute_capability((7, 2)).expect("nearest"),
SmResourceLimits::sm_70()
);
assert_eq!(
SmResourceLimits::from_compute_capability((8, 9)).expect("nearest"),
SmResourceLimits::sm_86()
);
}
#[test]
fn unknown_compute_capability_is_an_error() {
assert!(SmResourceLimits::from_compute_capability((5, 0)).is_err());
assert!(SmResourceLimits::from_compute_capability((10, 0)).is_err());
assert!(SmResourceLimits::from_compute_capability((0, 0)).is_err());
}
#[test]
fn architecture_constants_are_consistent() {
for limits in [
SmResourceLimits::sm_70(),
SmResourceLimits::sm_75(),
SmResourceLimits::sm_80(),
SmResourceLimits::sm_86(),
SmResourceLimits::sm_90(),
] {
assert_eq!(limits.warp_size, 32);
assert_eq!(limits.max_threads_per_block, 1024);
assert_eq!(limits.register_alloc_granularity, 256);
assert_eq!(limits.registers_per_sm, 65536);
assert!(limits.max_warps_per_sm > 0);
assert!(limits.max_blocks_per_sm > 0);
assert_eq!(
limits.max_threads_per_sm,
limits.max_warps_per_sm * limits.warp_size
);
}
assert_eq!(SmResourceLimits::sm_75().max_warps_per_sm, 32);
assert_eq!(SmResourceLimits::sm_75().max_blocks_per_sm, 16);
assert_eq!(SmResourceLimits::sm_86().max_warps_per_sm, 48);
assert_eq!(SmResourceLimits::sm_90().shared_mem_per_sm_bytes, 233_472);
}
#[test]
fn from_device_capabilities_uses_compute_capability() {
let caps = crate::backends::DeviceCapabilities {
name: "A100 (test)".to_string(),
total_memory: 0,
available_memory: 0,
supports_f16: true,
supports_bf16: true,
supports_tensor_cores: true,
max_threads_per_block: 1024,
max_shared_memory_per_block: 49152,
multiprocessor_count: 108,
compute_capability: (8, 0),
};
let limits = SmResourceLimits::from_device_capabilities(&caps).expect("cuda device");
assert_eq!(limits.max_warps_per_sm, 64);
assert_eq!(limits.max_blocks_per_sm, 32);
assert_eq!(limits.max_threads_per_block, 1024);
}
#[test]
fn from_device_capabilities_rejects_non_cuda_devices() {
let caps = crate::backends::DeviceCapabilities {
name: "CPU (test)".to_string(),
total_memory: 0,
available_memory: 0,
supports_f16: false,
supports_bf16: false,
supports_tensor_cores: false,
max_threads_per_block: 1,
max_shared_memory_per_block: 0,
multiprocessor_count: 1,
compute_capability: (0, 0),
};
assert!(SmResourceLimits::from_device_capabilities(&caps).is_err());
}
#[test]
fn occupancy_for_launch_matches_direct_calculation() {
let limits = SmResourceLimits::sm_80();
let config = crate::backends::LaunchConfig {
grid_size: (128, 1, 1),
block_size: (256, 1, 1),
shared_memory_size: 0,
stream: None,
};
let result = occupancy_for_launch(&config, 32, &limits).expect("valid launch");
assert_eq!(result.active_blocks_per_sm, 8);
assert!(approx(result.occupancy, 1.0));
}
}