mnemosyne_core/
kernel_budget.rs1#![cfg_attr(
13 test,
14 expect(
15 clippy::unwrap_used,
16 reason = "MNEM-UNWRAP-1: test scope, an unmet precondition in a test is a test failure"
17 )
18)]
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub struct KernelResourceBudget {
29 registers_per_thread: u32,
30 shared_mem_per_block_bytes: usize,
31 threads_per_block: u32,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct OccupancyLimits {
41 pub by_registers: u32,
43 pub by_shared_mem: u32,
45 pub by_threads: u32,
47}
48
49impl OccupancyLimits {
50 #[must_use]
55 #[inline]
56 pub const fn blocks_per_unit(self) -> u32 {
57 let mut limit = self.by_registers;
58 if self.by_shared_mem < limit {
59 limit = self.by_shared_mem;
60 }
61 if self.by_threads < limit {
62 limit = self.by_threads;
63 }
64 limit
65 }
66}
67
68#[inline]
72const fn div_capacity_or_max(capacity: u64, per_block: u64) -> u32 {
73 if per_block == 0 || capacity == 0 {
74 return u32::MAX;
75 }
76 let blocks = capacity / per_block;
77 if blocks > u32::MAX as u64 {
78 u32::MAX
79 } else {
80 blocks as u32
81 }
82}
83
84impl KernelResourceBudget {
85 #[must_use]
87 pub const fn new(
88 registers_per_thread: u32,
89 shared_mem_per_block_bytes: usize,
90 threads_per_block: u32,
91 ) -> Option<Self> {
92 if threads_per_block == 0 {
93 return None;
94 }
95 Some(Self {
96 registers_per_thread,
97 shared_mem_per_block_bytes,
98 threads_per_block,
99 })
100 }
101
102 #[must_use]
104 #[inline]
105 pub const fn registers_per_thread(self) -> u32 {
106 self.registers_per_thread
107 }
108
109 #[must_use]
111 #[inline]
112 pub const fn shared_mem_per_block_bytes(self) -> usize {
113 self.shared_mem_per_block_bytes
114 }
115
116 #[must_use]
118 #[inline]
119 pub const fn threads_per_block(self) -> u32 {
120 self.threads_per_block
121 }
122
123 #[must_use]
130 #[inline]
131 pub const fn registers_per_block(self) -> u64 {
132 (self.registers_per_thread as u64) * (self.threads_per_block as u64)
133 }
134
135 #[must_use]
141 pub const fn blocks_limited_by_registers(self, unit_registers: u32) -> u32 {
142 div_capacity_or_max(unit_registers as u64, self.registers_per_block())
143 }
144
145 #[must_use]
148 pub const fn blocks_limited_by_shared_mem(self, unit_shared_mem_bytes: usize) -> u32 {
149 div_capacity_or_max(
150 unit_shared_mem_bytes as u64,
151 self.shared_mem_per_block_bytes as u64,
152 )
153 }
154
155 #[must_use]
158 pub const fn blocks_limited_by_threads(self, max_threads_per_unit: u32) -> u32 {
159 if max_threads_per_unit == 0 {
160 return u32::MAX;
161 }
162 max_threads_per_unit / self.threads_per_block
163 }
164
165 #[must_use]
173 pub const fn occupancy_limits(
174 self,
175 unit_registers: u32,
176 unit_shared_mem_bytes: usize,
177 max_threads_per_unit: u32,
178 ) -> OccupancyLimits {
179 OccupancyLimits {
180 by_registers: self.blocks_limited_by_registers(unit_registers),
181 by_shared_mem: self.blocks_limited_by_shared_mem(unit_shared_mem_bytes),
182 by_threads: self.blocks_limited_by_threads(max_threads_per_unit),
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 const UNIT_REGISTERS: u32 = 65_536;
193 const UNIT_SHARED: usize = 102_400;
194 const UNIT_THREADS: u32 = 1_536;
195
196 #[test]
197 fn zero_thread_budget_is_rejected() {
198 assert!(KernelResourceBudget::new(32, 0, 0).is_none());
199 assert!(KernelResourceBudget::new(32, 0, 1).is_some());
200 }
201
202 #[test]
203 fn register_limiter_matches_closed_form() {
204 let budget = KernelResourceBudget::new(64, 0, 256).unwrap();
206 assert_eq!(budget.registers_per_block(), 16_384);
207 assert_eq!(budget.blocks_limited_by_registers(UNIT_REGISTERS), 4);
208 }
209
210 #[test]
211 fn shared_mem_limiter_matches_closed_form() {
212 let budget = KernelResourceBudget::new(0, 16 * 1024, 128).unwrap();
214 assert_eq!(budget.blocks_limited_by_shared_mem(UNIT_SHARED), 6);
215 }
216
217 #[test]
218 fn thread_limiter_matches_closed_form() {
219 let budget = KernelResourceBudget::new(0, 0, 256).unwrap();
221 assert_eq!(budget.blocks_limited_by_threads(UNIT_THREADS), 6);
222 }
223
224 #[test]
225 fn binding_constraint_is_the_minimum() {
226 let budget = KernelResourceBudget::new(64, 16 * 1024, 256).unwrap();
228 let limits = budget.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
229 assert_eq!(limits.by_registers, 4);
230 assert_eq!(limits.by_shared_mem, 6);
231 assert_eq!(limits.by_threads, 6);
232 assert_eq!(limits.blocks_per_unit(), 4);
233 }
234
235 #[test]
236 fn unreported_capacities_are_unconstrained_not_fabricated() {
237 let budget = KernelResourceBudget::new(64, 16 * 1024, 256).unwrap();
238 let limits = budget.occupancy_limits(0, 0, 0);
239 assert_eq!(limits.by_registers, u32::MAX);
240 assert_eq!(limits.by_shared_mem, u32::MAX);
241 assert_eq!(limits.by_threads, u32::MAX);
242 assert_eq!(limits.blocks_per_unit(), u32::MAX);
243 }
244
245 #[test]
246 fn zero_resource_budgets_are_unconstrained_by_that_resource() {
247 let budget = KernelResourceBudget::new(0, 0, 256).unwrap();
248 let limits = budget.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
249 assert_eq!(limits.by_registers, u32::MAX);
250 assert_eq!(limits.by_shared_mem, u32::MAX);
251 assert_eq!(limits.blocks_per_unit(), 6); }
253
254 #[test]
255 fn limits_are_const_evaluable() {
256 const BUDGET: KernelResourceBudget = match KernelResourceBudget::new(64, 16 * 1024, 256) {
258 Some(budget) => budget,
259 None => panic!("non-zero thread count"),
260 };
261 const LIMITS: OccupancyLimits =
262 BUDGET.occupancy_limits(UNIT_REGISTERS, UNIT_SHARED, UNIT_THREADS);
263 const BLOCKS: u32 = LIMITS.blocks_per_unit();
264 assert_eq!(BLOCKS, 4);
265 }
266}