use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use onnx_runtime_ep_api::Kernel;
use onnx_runtime_ir::{DataType, DeviceId};
pub type CachedKernel = Arc<Mutex<Box<dyn Kernel>>>;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct KernelCacheKey {
pub op_type: String,
pub domain: String,
pub opset: u64,
pub input_shapes: Vec<Vec<usize>>,
pub input_dtypes: Vec<DataType>,
pub device: DeviceId,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CacheStats {
pub entries: usize,
pub hits: u64,
pub misses: u64,
}
pub struct KernelCache {
capacity: usize,
map: HashMap<KernelCacheKey, CachedKernel>,
order: VecDeque<KernelCacheKey>,
hits: u64,
misses: u64,
}
impl KernelCache {
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
map: HashMap::new(),
order: VecDeque::new(),
hits: 0,
misses: 0,
}
}
pub fn get_or_create<E>(
&mut self,
key: KernelCacheKey,
create: impl FnOnce() -> Result<Box<dyn Kernel>, E>,
) -> Result<CachedKernel, E> {
if let Some(kernel) = self.map.get(&key) {
let kernel = kernel.clone();
self.hits += 1;
self.touch(&key);
return Ok(kernel);
}
self.misses += 1;
let kernel: CachedKernel = Arc::new(Mutex::new(create()?));
self.map.insert(key.clone(), kernel.clone());
self.order.push_back(key);
self.evict_if_needed();
Ok(kernel)
}
pub fn stats(&self) -> CacheStats {
CacheStats {
entries: self.map.len(),
hits: self.hits,
misses: self.misses,
}
}
fn touch(&mut self, key: &KernelCacheKey) {
if let Some(pos) = self.order.iter().position(|k| k == key)
&& let Some(k) = self.order.remove(pos)
{
self.order.push_back(k);
}
}
fn evict_if_needed(&mut self) {
while self.map.len() > self.capacity {
if let Some(lru) = self.order.pop_front() {
self.map.remove(&lru);
} else {
break;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use onnx_runtime_ep_api::{Result as EpResult, TensorMut, TensorView};
struct NopKernel;
impl Kernel for NopKernel {
fn execute(&self, _inputs: &[TensorView], _outputs: &mut [TensorMut]) -> EpResult<()> {
Ok(())
}
}
fn key(op: &str, shape: Vec<usize>) -> KernelCacheKey {
KernelCacheKey {
op_type: op.to_string(),
domain: String::new(),
opset: 26,
input_shapes: vec![shape],
input_dtypes: vec![DataType::Float32],
device: DeviceId::cpu(),
}
}
#[test]
fn miss_then_hit_same_key() {
let mut cache = KernelCache::new(8);
let k = key("Add", vec![2, 3]);
let mut created = 0;
let _ = cache
.get_or_create::<()>(k.clone(), || {
created += 1;
Ok(Box::new(NopKernel))
})
.unwrap();
let _ = cache
.get_or_create::<()>(k.clone(), || {
created += 1;
Ok(Box::new(NopKernel))
})
.unwrap();
assert_eq!(created, 1, "second dispatch must reuse the cached kernel");
assert_eq!(cache.stats().hits, 1);
assert_eq!(cache.stats().misses, 1);
assert_eq!(cache.stats().entries, 1);
}
#[test]
fn distinct_shapes_are_distinct_entries() {
let mut cache = KernelCache::new(8);
let _ = cache
.get_or_create::<()>(key("Add", vec![2, 3]), || Ok(Box::new(NopKernel)))
.unwrap();
let _ = cache
.get_or_create::<()>(key("Add", vec![4, 5]), || Ok(Box::new(NopKernel)))
.unwrap();
assert_eq!(cache.stats().entries, 2);
assert_eq!(cache.stats().misses, 2);
}
#[test]
fn lru_evicts_oldest_over_capacity() {
let mut cache = KernelCache::new(2);
let a = key("Add", vec![1]);
let b = key("Mul", vec![1]);
let c = key("Sub", vec![1]);
let _ = cache.get_or_create::<()>(a.clone(), || Ok(Box::new(NopKernel))).unwrap();
let _ = cache.get_or_create::<()>(b.clone(), || Ok(Box::new(NopKernel))).unwrap();
let _ = cache.get_or_create::<()>(a.clone(), || Ok(Box::new(NopKernel))).unwrap();
let _ = cache.get_or_create::<()>(c.clone(), || Ok(Box::new(NopKernel))).unwrap();
assert_eq!(cache.stats().entries, 2);
let mut recreated = 0;
let _ = cache
.get_or_create::<()>(b, || {
recreated += 1;
Ok(Box::new(NopKernel))
})
.unwrap();
assert_eq!(recreated, 1);
}
}