#![cfg_attr(
test,
expect(
clippy::unwrap_used,
reason = "MNEM-UNWRAP-1: test scope, an unmet precondition in a test is a test failure"
)
)]
#[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
}
}
#[inline]
const fn div_capacity_or_max(capacity: u64, per_block: u64) -> u32 {
if per_block == 0 || capacity == 0 {
return u32::MAX;
}
let blocks = capacity / per_block;
if blocks > u32::MAX as u64 {
u32::MAX
} else {
blocks as u32
}
}
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 {
div_capacity_or_max(unit_registers as u64, self.registers_per_block())
}
#[must_use]
pub const fn blocks_limited_by_shared_mem(self, unit_shared_mem_bytes: usize) -> u32 {
div_capacity_or_max(
unit_shared_mem_bytes as u64,
self.shared_mem_per_block_bytes as u64,
)
}
#[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);
}
}