mnemosyne_core/
kernel_budget.rs1#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct OccupancyLimits {
34 pub by_registers: u32,
36 pub by_shared_mem: u32,
38 pub by_threads: u32,
40}
41
42impl OccupancyLimits {
43 #[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 #[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 #[must_use]
81 #[inline]
82 pub const fn registers_per_thread(self) -> u32 {
83 self.registers_per_thread
84 }
85
86 #[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 #[must_use]
95 #[inline]
96 pub const fn threads_per_block(self) -> u32 {
97 self.threads_per_block
98 }
99
100 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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); }
244
245 #[test]
246 fn limits_are_const_evaluable() {
247 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}