1use std::collections::HashMap;
17
18use crate::error::{LevelZeroError, LevelZeroResult};
19
20#[must_use]
29pub fn spirv_cache_key(spirv: &[u32], build_flags: &str) -> u64 {
30 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
31 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
32 let mut hash = FNV_OFFSET;
33 for &word in spirv {
34 for byte in word.to_le_bytes() {
35 hash ^= u64::from(byte);
36 hash = hash.wrapping_mul(FNV_PRIME);
37 }
38 }
39 for byte in build_flags.as_bytes() {
41 hash ^= u64::from(*byte);
42 hash = hash.wrapping_mul(FNV_PRIME);
43 }
44 hash
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct NativeBinary {
52 pub bytes: Vec<u8>,
54 pub build_flags: String,
56}
57
58impl NativeBinary {
59 #[must_use]
61 pub fn new(bytes: Vec<u8>, build_flags: impl Into<String>) -> Self {
62 Self {
63 bytes,
64 build_flags: build_flags.into(),
65 }
66 }
67
68 #[must_use]
70 pub fn len(&self) -> usize {
71 self.bytes.len()
72 }
73
74 #[must_use]
76 pub fn is_empty(&self) -> bool {
77 self.bytes.is_empty()
78 }
79}
80
81#[derive(Debug, Default)]
92pub struct ModuleBinaryCache {
93 entries: HashMap<u64, NativeBinary>,
94 hits: u64,
95 misses: u64,
96}
97
98impl ModuleBinaryCache {
99 #[must_use]
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 #[must_use]
107 pub fn len(&self) -> usize {
108 self.entries.len()
109 }
110
111 #[must_use]
113 pub fn is_empty(&self) -> bool {
114 self.entries.is_empty()
115 }
116
117 #[must_use]
119 pub fn hits(&self) -> u64 {
120 self.hits
121 }
122
123 #[must_use]
125 pub fn misses(&self) -> u64 {
126 self.misses
127 }
128
129 #[must_use]
131 pub fn hit_rate(&self) -> f64 {
132 let total = self.hits + self.misses;
133 if total == 0 {
134 0.0
135 } else {
136 self.hits as f64 / total as f64
137 }
138 }
139
140 pub fn lookup(&mut self, spirv: &[u32], build_flags: &str) -> Option<&NativeBinary> {
143 let key = spirv_cache_key(spirv, build_flags);
144 if self.entries.contains_key(&key) {
145 self.hits += 1;
146 self.entries.get(&key)
147 } else {
148 self.misses += 1;
149 None
150 }
151 }
152
153 pub fn insert(&mut self, spirv: &[u32], binary: NativeBinary) -> u64 {
158 let key = spirv_cache_key(spirv, &binary.build_flags);
159 self.entries.insert(key, binary);
160 key
161 }
162
163 #[must_use]
165 pub fn get(&self, key: u64) -> Option<&NativeBinary> {
166 self.entries.get(&key)
167 }
168
169 pub fn clear(&mut self) {
171 self.entries.clear();
172 }
173
174 #[must_use]
176 pub fn total_bytes(&self) -> usize {
177 self.entries.values().map(NativeBinary::len).sum()
178 }
179}
180
181#[derive(Debug, Clone)]
192pub struct EuOccupancyAdvisor {
193 min_wg: u32,
194 max_wg: u32,
195 eu_count: u32,
196}
197
198impl EuOccupancyAdvisor {
199 pub fn new(eu_count: u32, min_wg: u32, max_wg: u32) -> LevelZeroResult<Self> {
202 if eu_count == 0 {
203 return Err(LevelZeroError::InvalidArgument(
204 "EU count must be > 0".into(),
205 ));
206 }
207 if min_wg == 0 || max_wg == 0 || !min_wg.is_power_of_two() || !max_wg.is_power_of_two() {
208 return Err(LevelZeroError::InvalidArgument(
209 "workgroup-size bounds must be non-zero powers of two".into(),
210 ));
211 }
212 if min_wg > max_wg {
213 return Err(LevelZeroError::InvalidArgument(
214 "min workgroup size exceeds max".into(),
215 ));
216 }
217 Ok(Self {
218 min_wg,
219 max_wg,
220 eu_count,
221 })
222 }
223
224 #[must_use]
226 pub fn eu_count(&self) -> u32 {
227 self.eu_count
228 }
229
230 #[must_use]
238 pub fn recommend_workgroup_size(&self, utilisation: f64) -> u32 {
239 let u = utilisation.clamp(0.0, 1.0);
240 let span_log2 = (self.max_wg / self.min_wg).trailing_zeros();
243 let shift = (u * f64::from(span_log2)).round() as u32;
244 let candidate = (self.max_wg >> shift).max(self.min_wg);
245 let snapped = if candidate.is_power_of_two() {
247 candidate
248 } else {
249 candidate.next_power_of_two()
250 };
251 snapped.clamp(self.min_wg, self.max_wg)
252 }
253
254 #[must_use]
258 pub fn estimated_resident_workgroups(&self, workgroup_size: u32, subgroup_size: u32) -> u32 {
259 if workgroup_size == 0 || subgroup_size == 0 {
260 return 0;
261 }
262 let subgroups_per_wg = workgroup_size.div_ceil(subgroup_size).max(1);
263 (self.eu_count / subgroups_per_wg).max(1)
265 }
266}
267
268#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn cache_key_is_deterministic_and_flag_sensitive() {
276 let spirv = [0x0723_0203, 0x0001_0200, 1, 2, 3];
277 let k1 = spirv_cache_key(&spirv, "");
278 let k2 = spirv_cache_key(&spirv, "");
279 assert_eq!(k1, k2, "same input → same key");
280 let k3 = spirv_cache_key(&spirv, "-ze-opt-disable");
281 assert_ne!(k1, k3, "build flags must change the key");
282 let mut other = spirv;
283 other[4] = 99;
284 assert_ne!(
285 k1,
286 spirv_cache_key(&other, ""),
287 "different SPIR-V → different key"
288 );
289 }
290
291 #[test]
292 fn native_binary_fields() {
293 let b = NativeBinary::new(vec![1, 2, 3, 4], "-cl-fast-relaxed-math");
294 assert_eq!(b.len(), 4);
295 assert!(!b.is_empty());
296 assert_eq!(b.build_flags, "-cl-fast-relaxed-math");
297 assert!(NativeBinary::new(vec![], "").is_empty());
298 }
299
300 #[test]
301 fn cache_miss_then_hit() {
302 let mut cache = ModuleBinaryCache::new();
303 assert!(cache.is_empty());
304 let spirv = [0x0723_0203, 1, 2, 3, 4];
305
306 assert!(cache.lookup(&spirv, "").is_none());
308 assert_eq!(cache.misses(), 1);
309 assert_eq!(cache.hits(), 0);
310
311 let key = cache.insert(&spirv, NativeBinary::new(vec![9, 8, 7], ""));
313 assert_eq!(cache.len(), 1);
314 let got = cache.lookup(&spirv, "").expect("hit");
315 assert_eq!(got.bytes, vec![9, 8, 7]);
316 assert_eq!(cache.hits(), 1);
317 assert_eq!(cache.get(key).map(NativeBinary::len), Some(3));
318 assert_eq!(cache.total_bytes(), 3);
319 }
320
321 #[test]
322 fn cache_hit_rate_and_clear() {
323 let mut cache = ModuleBinaryCache::new();
324 assert_eq!(cache.hit_rate(), 0.0);
325 let spirv = [1u32, 2, 3, 4, 5];
326 cache.insert(&spirv, NativeBinary::new(vec![1], ""));
327 assert!(cache.lookup(&spirv, "").is_some()); assert!(cache.lookup(&[9u32, 9, 9, 9, 9], "").is_none()); assert!(cache.lookup(&[9u32, 9, 9, 9, 9], "").is_none()); assert!((cache.hit_rate() - (1.0 / 3.0)).abs() < 1e-9);
332 cache.clear();
333 assert!(cache.is_empty());
334 assert_eq!(cache.hits(), 1);
336 }
337
338 #[test]
339 fn cache_distinguishes_build_flags() {
340 let mut cache = ModuleBinaryCache::new();
341 let spirv = [1u32, 2, 3, 4, 5];
342 cache.insert(&spirv, NativeBinary::new(vec![1], ""));
343 cache.insert(&spirv, NativeBinary::new(vec![2, 2], "-O3"));
344 assert_eq!(cache.len(), 2);
346 assert_eq!(cache.lookup(&spirv, "").unwrap().bytes, vec![1]);
347 assert_eq!(cache.lookup(&spirv, "-O3").unwrap().bytes, vec![2, 2]);
348 }
349
350 #[test]
351 fn occupancy_advisor_idle_vs_busy() {
352 let adv = EuOccupancyAdvisor::new(512, 8, 256).unwrap();
353 assert_eq!(adv.eu_count(), 512);
354 assert_eq!(adv.recommend_workgroup_size(0.0), 256);
356 assert_eq!(adv.recommend_workgroup_size(1.0), 8);
358 let mid = adv.recommend_workgroup_size(0.5);
360 assert!((8..=256).contains(&mid));
361 assert!(mid.is_power_of_two());
362 }
363
364 #[test]
365 fn occupancy_advisor_monotonic() {
366 let adv = EuOccupancyAdvisor::new(128, 16, 512).unwrap();
367 let low = adv.recommend_workgroup_size(0.1);
368 let high = adv.recommend_workgroup_size(0.9);
369 assert!(
370 low >= high,
371 "busier device should not recommend a larger workgroup (low={low}, high={high})"
372 );
373 assert_eq!(adv.recommend_workgroup_size(-5.0), 512);
375 assert_eq!(adv.recommend_workgroup_size(99.0), 16);
376 }
377
378 #[test]
379 fn occupancy_advisor_resident_estimate() {
380 let adv = EuOccupancyAdvisor::new(512, 8, 256).unwrap();
381 assert_eq!(adv.estimated_resident_workgroups(32, 16), 256);
383 assert_eq!(adv.estimated_resident_workgroups(0, 16), 0);
385 assert_eq!(adv.estimated_resident_workgroups(32, 0), 0);
386 }
387
388 #[test]
389 fn occupancy_advisor_rejects_bad_args() {
390 assert!(EuOccupancyAdvisor::new(0, 8, 256).is_err());
391 assert!(EuOccupancyAdvisor::new(128, 0, 256).is_err());
392 assert!(
393 EuOccupancyAdvisor::new(128, 24, 256).is_err(),
394 "non-pow2 min"
395 );
396 assert!(EuOccupancyAdvisor::new(128, 512, 256).is_err(), "min > max");
397 }
398}