Skip to main content

mnemosyne_core/
kernel_budget.rs

1//! Kernel resource budgets for GPU occupancy planning (atlas ADR 0002).
2//!
3//! GPU register files and shared memory are **not host-allocatable**: the
4//! kernel compiler assigns registers and kernels declare shared memory at
5//! launch. Mnemosyne therefore owns the *budget vocabulary and accounting* —
6//! how many registers per thread and shared bytes per block a kernel
7//! requires — which moirai's occupancy planner intersects with per-unit
8//! capacities (themis `GpuTopology` accessors: `registers_per_unit`,
9//! `shared_mem_per_unit_bytes`, `max_threads_per_unit`) to derive launch
10//! shapes. This module is `no_std`, dependency-free, and fully `const`:
11//! every limiter resolves at compile time when the budget is a constant.
12
13/// A kernel's per-launch resource requirements.
14///
15/// Constructed with [`KernelResourceBudget::new`], which rejects a zero
16/// thread count (a launch with no threads is meaningless and would poison
17/// the occupancy arithmetic). A `registers_per_thread` or
18/// `shared_mem_per_block_bytes` of zero means the kernel uses none of that
19/// resource and is therefore unconstrained by it.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub struct KernelResourceBudget {
22    registers_per_thread: u32,
23    shared_mem_per_block_bytes: usize,
24    threads_per_block: u32,
25}
26
27/// Result of intersecting a budget with one compute unit's capacities.
28///
29/// `u32::MAX` in a limiter means "unconstrained by this resource" — either
30/// the kernel uses none of it, or the capacity is unreported (zero) and the
31/// caller must decide policy rather than have a fabricated bound.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct OccupancyLimits {
34    /// Blocks per unit limited by the register file.
35    pub by_registers: u32,
36    /// Blocks per unit limited by shared memory.
37    pub by_shared_mem: u32,
38    /// Blocks per unit limited by resident-thread capacity.
39    pub by_threads: u32,
40}
41
42impl OccupancyLimits {
43    /// The binding constraint: the minimum of the three limiters.
44    ///
45    /// Returns `u32::MAX` only when every dimension is unconstrained, which
46    /// the caller must treat as "no information", not "infinite blocks".
47    #[must_use]
48    #[inline]
49    pub const fn blocks_per_unit(self) -> u32 {
50        let mut limit = self.by_registers;
51        if self.by_shared_mem < limit {
52            limit = self.by_shared_mem;
53        }
54        if self.by_threads < limit {
55            limit = self.by_threads;
56        }
57        limit
58    }
59}
60
61impl KernelResourceBudget {
62    /// Construct a budget. Returns `None` when `threads_per_block` is zero.
63    #[must_use]
64    pub const fn new(
65        registers_per_thread: u32,
66        shared_mem_per_block_bytes: usize,
67        threads_per_block: u32,
68    ) -> Option<Self> {
69        if threads_per_block == 0 {
70            return None;
71        }
72        Some(Self {
73            registers_per_thread,
74            shared_mem_per_block_bytes,
75            threads_per_block,
76        })
77    }
78
79    /// Registers each thread requires (compiler-reported).
80    #[must_use]
81    #[inline]
82    pub const fn registers_per_thread(self) -> u32 {
83        self.registers_per_thread
84    }
85
86    /// Shared-memory bytes each block declares at launch.
87    #[must_use]
88    #[inline]
89    pub const fn shared_mem_per_block_bytes(self) -> usize {
90        self.shared_mem_per_block_bytes
91    }
92
93    /// Threads per block of the planned launch shape.
94    #[must_use]
95    #[inline]
96    pub const fn threads_per_block(self) -> u32 {
97        self.threads_per_block
98    }
99
100    /// Registers one block consumes: `registers_per_thread · threads_per_block`.
101    ///
102    /// The product is computed in `u64` after widening both `u32` factors, so it
103    /// is exact for every input: the maximum product `(2^32 - 1)^2 = 2^64 -
104    /// 2^33 + 1` is strictly less than `u64::MAX`, so the multiplication never
105    /// overflows and no saturation or wrapping is possible.
106    #[must_use]
107    #[inline]
108    pub const fn registers_per_block(self) -> u64 {
109        (self.registers_per_thread as u64) * (self.threads_per_block as u64)
110    }
111
112    /// Blocks per unit limited by a register file of `unit_registers`.
113    ///
114    /// Unconstrained (`u32::MAX`) when the kernel uses no registers or the
115    /// capacity is unreported (zero) — an unreported capacity must surface as
116    /// "no information" for the planner, never a fabricated bound.
117    #[must_use]
118    pub const fn blocks_limited_by_registers(self, unit_registers: u32) -> u32 {
119        let per_block = self.registers_per_block();
120        if per_block == 0 || unit_registers == 0 {
121            return u32::MAX;
122        }
123        let blocks = (unit_registers as u64) / per_block;
124        if blocks > u32::MAX as u64 {
125            u32::MAX
126        } else {
127            blocks as u32
128        }
129    }
130
131    /// Blocks per unit limited by `unit_shared_mem_bytes` of shared memory.
132    /// Same unconstrained semantics as the register limiter.
133    #[must_use]
134    pub const fn blocks_limited_by_shared_mem(self, unit_shared_mem_bytes: usize) -> u32 {
135        if self.shared_mem_per_block_bytes == 0 || unit_shared_mem_bytes == 0 {
136            return u32::MAX;
137        }
138        let blocks = unit_shared_mem_bytes / self.shared_mem_per_block_bytes;
139        if blocks > u32::MAX as usize {
140            u32::MAX
141        } else {
142            blocks as u32
143        }
144    }
145
146    /// Blocks per unit limited by `max_threads_per_unit` resident threads.
147    /// Unconstrained when the capacity is unreported (zero).
148    #[must_use]
149    pub const fn blocks_limited_by_threads(self, max_threads_per_unit: u32) -> u32 {
150        if max_threads_per_unit == 0 {
151            return u32::MAX;
152        }
153        max_threads_per_unit / self.threads_per_block
154    }
155
156    /// Intersect this budget with one compute unit's capacities.
157    ///
158    /// The capacities are the themis `GpuTopology` per-unit accessors
159    /// (`registers_per_unit()`, `shared_mem_per_unit_bytes()`,
160    /// `max_threads_per_unit()`), passed as plain quantities so this crate
161    /// stays `no_std` and dependency-free; the typed pairing lives in
162    /// moirai's occupancy planner.
163    #[must_use]
164    pub const fn occupancy_limits(
165        self,
166        unit_registers: u32,
167        unit_shared_mem_bytes: usize,
168        max_threads_per_unit: u32,
169    ) -> OccupancyLimits {
170        OccupancyLimits {
171            by_registers: self.blocks_limited_by_registers(unit_registers),
172            by_shared_mem: self.blocks_limited_by_shared_mem(unit_shared_mem_bytes),
173            by_threads: self.blocks_limited_by_threads(max_threads_per_unit),
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    // Ampere-class unit capacities used as closed-form fixtures.
183    const UNIT_REGISTERS: u32 = 65_536;
184    const UNIT_SHARED: usize = 102_400;
185    const UNIT_THREADS: u32 = 1_536;
186
187    #[test]
188    fn zero_thread_budget_is_rejected() {
189        assert!(KernelResourceBudget::new(32, 0, 0).is_none());
190        assert!(KernelResourceBudget::new(32, 0, 1).is_some());
191    }
192
193    #[test]
194    fn register_limiter_matches_closed_form() {
195        // 64 regs/thread × 256 threads = 16384 regs/block; 65536/16384 = 4.
196        let budget = KernelResourceBudget::new(64, 0, 256).unwrap();
197        assert_eq!(budget.registers_per_block(), 16_384);
198        assert_eq!(budget.blocks_limited_by_registers(UNIT_REGISTERS), 4);
199    }
200
201    #[test]
202    fn shared_mem_limiter_matches_closed_form() {
203        // 100 KiB unit / 16 KiB per block = 6 blocks (floor).
204        let budget = KernelResourceBudget::new(0, 16 * 1024, 128).unwrap();
205        assert_eq!(budget.blocks_limited_by_shared_mem(UNIT_SHARED), 6);
206    }
207
208    #[test]
209    fn thread_limiter_matches_closed_form() {
210        // 1536 resident / 256 per block = 6 blocks.
211        let budget = KernelResourceBudget::new(0, 0, 256).unwrap();
212        assert_eq!(budget.blocks_limited_by_threads(UNIT_THREADS), 6);
213    }
214
215    #[test]
216    fn binding_constraint_is_the_minimum() {
217        // Registers bind at 4, shared at 6, threads at 6 -> 4.
218        let budget = KernelResourceBudget::new(64, 16 * 1024, 256).unwrap();
219        let limits = budget.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
220        assert_eq!(limits.by_registers, 4);
221        assert_eq!(limits.by_shared_mem, 6);
222        assert_eq!(limits.by_threads, 6);
223        assert_eq!(limits.blocks_per_unit(), 4);
224    }
225
226    #[test]
227    fn unreported_capacities_are_unconstrained_not_fabricated() {
228        let budget = KernelResourceBudget::new(64, 16 * 1024, 256).unwrap();
229        let limits = budget.occupancy_limits(0, 0, 0);
230        assert_eq!(limits.by_registers, u32::MAX);
231        assert_eq!(limits.by_shared_mem, u32::MAX);
232        assert_eq!(limits.by_threads, u32::MAX);
233        assert_eq!(limits.blocks_per_unit(), u32::MAX);
234    }
235
236    #[test]
237    fn zero_resource_budgets_are_unconstrained_by_that_resource() {
238        let budget = KernelResourceBudget::new(0, 0, 256).unwrap();
239        let limits = budget.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
240        assert_eq!(limits.by_registers, u32::MAX);
241        assert_eq!(limits.by_shared_mem, u32::MAX);
242        assert_eq!(limits.blocks_per_unit(), 6); // threads bind
243    }
244
245    #[test]
246    fn limits_are_const_evaluable() {
247        // The whole pipeline resolves at compile time for constant budgets.
248        const BUDGET: KernelResourceBudget = match KernelResourceBudget::new(64, 16 * 1024, 256) {
249            Some(budget) => budget,
250            None => panic!("non-zero thread count"),
251        };
252        const LIMITS: OccupancyLimits =
253            BUDGET.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
254        const BLOCKS: u32 = LIMITS.blocks_per_unit();
255        assert_eq!(BLOCKS, 4);
256    }
257}