use std::collections::HashMap;
use crate::error::{LevelZeroError, LevelZeroResult};
#[must_use]
pub fn spirv_cache_key(spirv: &[u32], build_flags: &str) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = FNV_OFFSET;
for &word in spirv {
for byte in word.to_le_bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
}
for byte in build_flags.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeBinary {
pub bytes: Vec<u8>,
pub build_flags: String,
}
impl NativeBinary {
#[must_use]
pub fn new(bytes: Vec<u8>, build_flags: impl Into<String>) -> Self {
Self {
bytes,
build_flags: build_flags.into(),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
#[derive(Debug, Default)]
pub struct ModuleBinaryCache {
entries: HashMap<u64, NativeBinary>,
hits: u64,
misses: u64,
}
impl ModuleBinaryCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn hits(&self) -> u64 {
self.hits
}
#[must_use]
pub fn misses(&self) -> u64 {
self.misses
}
#[must_use]
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
pub fn lookup(&mut self, spirv: &[u32], build_flags: &str) -> Option<&NativeBinary> {
let key = spirv_cache_key(spirv, build_flags);
if self.entries.contains_key(&key) {
self.hits += 1;
self.entries.get(&key)
} else {
self.misses += 1;
None
}
}
pub fn insert(&mut self, spirv: &[u32], binary: NativeBinary) -> u64 {
let key = spirv_cache_key(spirv, &binary.build_flags);
self.entries.insert(key, binary);
key
}
#[must_use]
pub fn get(&self, key: u64) -> Option<&NativeBinary> {
self.entries.get(&key)
}
pub fn clear(&mut self) {
self.entries.clear();
}
#[must_use]
pub fn total_bytes(&self) -> usize {
self.entries.values().map(NativeBinary::len).sum()
}
}
#[derive(Debug, Clone)]
pub struct EuOccupancyAdvisor {
min_wg: u32,
max_wg: u32,
eu_count: u32,
}
impl EuOccupancyAdvisor {
pub fn new(eu_count: u32, min_wg: u32, max_wg: u32) -> LevelZeroResult<Self> {
if eu_count == 0 {
return Err(LevelZeroError::InvalidArgument(
"EU count must be > 0".into(),
));
}
if min_wg == 0 || max_wg == 0 || !min_wg.is_power_of_two() || !max_wg.is_power_of_two() {
return Err(LevelZeroError::InvalidArgument(
"workgroup-size bounds must be non-zero powers of two".into(),
));
}
if min_wg > max_wg {
return Err(LevelZeroError::InvalidArgument(
"min workgroup size exceeds max".into(),
));
}
Ok(Self {
min_wg,
max_wg,
eu_count,
})
}
#[must_use]
pub fn eu_count(&self) -> u32 {
self.eu_count
}
#[must_use]
pub fn recommend_workgroup_size(&self, utilisation: f64) -> u32 {
let u = utilisation.clamp(0.0, 1.0);
let span_log2 = (self.max_wg / self.min_wg).trailing_zeros();
let shift = (u * f64::from(span_log2)).round() as u32;
let candidate = (self.max_wg >> shift).max(self.min_wg);
let snapped = if candidate.is_power_of_two() {
candidate
} else {
candidate.next_power_of_two()
};
snapped.clamp(self.min_wg, self.max_wg)
}
#[must_use]
pub fn estimated_resident_workgroups(&self, workgroup_size: u32, subgroup_size: u32) -> u32 {
if workgroup_size == 0 || subgroup_size == 0 {
return 0;
}
let subgroups_per_wg = workgroup_size.div_ceil(subgroup_size).max(1);
(self.eu_count / subgroups_per_wg).max(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_key_is_deterministic_and_flag_sensitive() {
let spirv = [0x0723_0203, 0x0001_0200, 1, 2, 3];
let k1 = spirv_cache_key(&spirv, "");
let k2 = spirv_cache_key(&spirv, "");
assert_eq!(k1, k2, "same input → same key");
let k3 = spirv_cache_key(&spirv, "-ze-opt-disable");
assert_ne!(k1, k3, "build flags must change the key");
let mut other = spirv;
other[4] = 99;
assert_ne!(
k1,
spirv_cache_key(&other, ""),
"different SPIR-V → different key"
);
}
#[test]
fn native_binary_fields() {
let b = NativeBinary::new(vec![1, 2, 3, 4], "-cl-fast-relaxed-math");
assert_eq!(b.len(), 4);
assert!(!b.is_empty());
assert_eq!(b.build_flags, "-cl-fast-relaxed-math");
assert!(NativeBinary::new(vec![], "").is_empty());
}
#[test]
fn cache_miss_then_hit() {
let mut cache = ModuleBinaryCache::new();
assert!(cache.is_empty());
let spirv = [0x0723_0203, 1, 2, 3, 4];
assert!(cache.lookup(&spirv, "").is_none());
assert_eq!(cache.misses(), 1);
assert_eq!(cache.hits(), 0);
let key = cache.insert(&spirv, NativeBinary::new(vec![9, 8, 7], ""));
assert_eq!(cache.len(), 1);
let got = cache.lookup(&spirv, "").expect("hit");
assert_eq!(got.bytes, vec![9, 8, 7]);
assert_eq!(cache.hits(), 1);
assert_eq!(cache.get(key).map(NativeBinary::len), Some(3));
assert_eq!(cache.total_bytes(), 3);
}
#[test]
fn cache_hit_rate_and_clear() {
let mut cache = ModuleBinaryCache::new();
assert_eq!(cache.hit_rate(), 0.0);
let spirv = [1u32, 2, 3, 4, 5];
cache.insert(&spirv, NativeBinary::new(vec![1], ""));
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);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.hits(), 1);
}
#[test]
fn cache_distinguishes_build_flags() {
let mut cache = ModuleBinaryCache::new();
let spirv = [1u32, 2, 3, 4, 5];
cache.insert(&spirv, NativeBinary::new(vec![1], ""));
cache.insert(&spirv, NativeBinary::new(vec![2, 2], "-O3"));
assert_eq!(cache.len(), 2);
assert_eq!(cache.lookup(&spirv, "").unwrap().bytes, vec![1]);
assert_eq!(cache.lookup(&spirv, "-O3").unwrap().bytes, vec![2, 2]);
}
#[test]
fn occupancy_advisor_idle_vs_busy() {
let adv = EuOccupancyAdvisor::new(512, 8, 256).unwrap();
assert_eq!(adv.eu_count(), 512);
assert_eq!(adv.recommend_workgroup_size(0.0), 256);
assert_eq!(adv.recommend_workgroup_size(1.0), 8);
let mid = adv.recommend_workgroup_size(0.5);
assert!((8..=256).contains(&mid));
assert!(mid.is_power_of_two());
}
#[test]
fn occupancy_advisor_monotonic() {
let adv = EuOccupancyAdvisor::new(128, 16, 512).unwrap();
let low = adv.recommend_workgroup_size(0.1);
let high = adv.recommend_workgroup_size(0.9);
assert!(
low >= high,
"busier device should not recommend a larger workgroup (low={low}, high={high})"
);
assert_eq!(adv.recommend_workgroup_size(-5.0), 512);
assert_eq!(adv.recommend_workgroup_size(99.0), 16);
}
#[test]
fn occupancy_advisor_resident_estimate() {
let adv = EuOccupancyAdvisor::new(512, 8, 256).unwrap();
assert_eq!(adv.estimated_resident_workgroups(32, 16), 256);
assert_eq!(adv.estimated_resident_workgroups(0, 16), 0);
assert_eq!(adv.estimated_resident_workgroups(32, 0), 0);
}
#[test]
fn occupancy_advisor_rejects_bad_args() {
assert!(EuOccupancyAdvisor::new(0, 8, 256).is_err());
assert!(EuOccupancyAdvisor::new(128, 0, 256).is_err());
assert!(
EuOccupancyAdvisor::new(128, 24, 256).is_err(),
"non-pow2 min"
);
assert!(EuOccupancyAdvisor::new(128, 512, 256).is_err(), "min > max");
}
}