#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct KernelResourceBudget {
registers_per_thread: u32,
shared_mem_per_block_bytes: usize,
threads_per_block: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OccupancyLimits {
pub by_registers: u32,
pub by_shared_mem: u32,
pub by_threads: u32,
}
impl OccupancyLimits {
#[must_use]
#[inline]
pub const fn blocks_per_unit(self) -> u32 {
let mut limit = self.by_registers;
if self.by_shared_mem < limit {
limit = self.by_shared_mem;
}
if self.by_threads < limit {
limit = self.by_threads;
}
limit
}
}
impl KernelResourceBudget {
#[must_use]
pub const fn new(
registers_per_thread: u32,
shared_mem_per_block_bytes: usize,
threads_per_block: u32,
) -> Option<Self> {
if threads_per_block == 0 {
return None;
}
Some(Self {
registers_per_thread,
shared_mem_per_block_bytes,
threads_per_block,
})
}
#[must_use]
#[inline]
pub const fn registers_per_thread(self) -> u32 {
self.registers_per_thread
}
#[must_use]
#[inline]
pub const fn shared_mem_per_block_bytes(self) -> usize {
self.shared_mem_per_block_bytes
}
#[must_use]
#[inline]
pub const fn threads_per_block(self) -> u32 {
self.threads_per_block
}
#[must_use]
#[inline]
pub const fn registers_per_block(self) -> u64 {
(self.registers_per_thread as u64) * (self.threads_per_block as u64)
}
#[must_use]
pub const fn blocks_limited_by_registers(self, unit_registers: u32) -> u32 {
let per_block = self.registers_per_block();
if per_block == 0 || unit_registers == 0 {
return u32::MAX;
}
let blocks = (unit_registers as u64) / per_block;
if blocks > u32::MAX as u64 {
u32::MAX
} else {
blocks as u32
}
}
#[must_use]
pub const fn blocks_limited_by_shared_mem(self, unit_shared_mem_bytes: usize) -> u32 {
if self.shared_mem_per_block_bytes == 0 || unit_shared_mem_bytes == 0 {
return u32::MAX;
}
let blocks = unit_shared_mem_bytes / self.shared_mem_per_block_bytes;
if blocks > u32::MAX as usize {
u32::MAX
} else {
blocks as u32
}
}
#[must_use]
pub const fn blocks_limited_by_threads(self, max_threads_per_unit: u32) -> u32 {
if max_threads_per_unit == 0 {
return u32::MAX;
}
max_threads_per_unit / self.threads_per_block
}
#[must_use]
pub const fn occupancy_limits(
self,
unit_registers: u32,
unit_shared_mem_bytes: usize,
max_threads_per_unit: u32,
) -> OccupancyLimits {
OccupancyLimits {
by_registers: self.blocks_limited_by_registers(unit_registers),
by_shared_mem: self.blocks_limited_by_shared_mem(unit_shared_mem_bytes),
by_threads: self.blocks_limited_by_threads(max_threads_per_unit),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const UNIT_REGISTERS: u32 = 65_536;
const UNIT_SHARED: usize = 102_400;
const UNIT_THREADS: u32 = 1_536;
#[test]
fn zero_thread_budget_is_rejected() {
assert!(KernelResourceBudget::new(32, 0, 0).is_none());
assert!(KernelResourceBudget::new(32, 0, 1).is_some());
}
#[test]
fn register_limiter_matches_closed_form() {
let budget = KernelResourceBudget::new(64, 0, 256).unwrap();
assert_eq!(budget.registers_per_block(), 16_384);
assert_eq!(budget.blocks_limited_by_registers(UNIT_REGISTERS), 4);
}
#[test]
fn shared_mem_limiter_matches_closed_form() {
let budget = KernelResourceBudget::new(0, 16 * 1024, 128).unwrap();
assert_eq!(budget.blocks_limited_by_shared_mem(UNIT_SHARED), 6);
}
#[test]
fn thread_limiter_matches_closed_form() {
let budget = KernelResourceBudget::new(0, 0, 256).unwrap();
assert_eq!(budget.blocks_limited_by_threads(UNIT_THREADS), 6);
}
#[test]
fn binding_constraint_is_the_minimum() {
let budget = KernelResourceBudget::new(64, 16 * 1024, 256).unwrap();
let limits = budget.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
assert_eq!(limits.by_registers, 4);
assert_eq!(limits.by_shared_mem, 6);
assert_eq!(limits.by_threads, 6);
assert_eq!(limits.blocks_per_unit(), 4);
}
#[test]
fn unreported_capacities_are_unconstrained_not_fabricated() {
let budget = KernelResourceBudget::new(64, 16 * 1024, 256).unwrap();
let limits = budget.occupancy_limits(0, 0, 0);
assert_eq!(limits.by_registers, u32::MAX);
assert_eq!(limits.by_shared_mem, u32::MAX);
assert_eq!(limits.by_threads, u32::MAX);
assert_eq!(limits.blocks_per_unit(), u32::MAX);
}
#[test]
fn zero_resource_budgets_are_unconstrained_by_that_resource() {
let budget = KernelResourceBudget::new(0, 0, 256).unwrap();
let limits = budget.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
assert_eq!(limits.by_registers, u32::MAX);
assert_eq!(limits.by_shared_mem, u32::MAX);
assert_eq!(limits.blocks_per_unit(), 6); }
#[test]
fn limits_are_const_evaluable() {
const BUDGET: KernelResourceBudget = match KernelResourceBudget::new(64, 16 * 1024, 256) {
Some(budget) => budget,
None => panic!("non-zero thread count"),
};
const LIMITS: OccupancyLimits =
BUDGET.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
const BLOCKS: u32 = LIMITS.blocks_per_unit();
assert_eq!(BLOCKS, 4);
}
}