onnx_runtime_eager/
cache.rs1use std::collections::{HashMap, VecDeque};
25use std::sync::{Arc, Mutex};
26
27use onnx_runtime_ep_api::Kernel;
28use onnx_runtime_ir::{DataType, DeviceId};
29
30pub type CachedKernel = Arc<Mutex<Box<dyn Kernel>>>;
33
34#[derive(Clone, PartialEq, Eq, Hash, Debug)]
41pub struct KernelCacheKey {
42 pub op_type: String,
43 pub domain: String,
44 pub opset: u64,
45 pub input_shapes: Vec<Vec<usize>>,
46 pub input_dtypes: Vec<DataType>,
47 pub device: DeviceId,
48}
49
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub struct CacheStats {
53 pub entries: usize,
55 pub hits: u64,
57 pub misses: u64,
59}
60
61pub struct KernelCache {
63 capacity: usize,
64 map: HashMap<KernelCacheKey, CachedKernel>,
65 order: VecDeque<KernelCacheKey>,
67 hits: u64,
68 misses: u64,
69}
70
71impl KernelCache {
72 pub fn new(capacity: usize) -> Self {
75 Self {
76 capacity: capacity.max(1),
77 map: HashMap::new(),
78 order: VecDeque::new(),
79 hits: 0,
80 misses: 0,
81 }
82 }
83
84 pub fn get_or_create<E>(
87 &mut self,
88 key: KernelCacheKey,
89 create: impl FnOnce() -> Result<Box<dyn Kernel>, E>,
90 ) -> Result<CachedKernel, E> {
91 if let Some(kernel) = self.map.get(&key) {
92 let kernel = kernel.clone();
93 self.hits += 1;
94 self.touch(&key);
95 return Ok(kernel);
96 }
97 self.misses += 1;
98 let kernel: CachedKernel = Arc::new(Mutex::new(create()?));
99 self.map.insert(key.clone(), kernel.clone());
100 self.order.push_back(key);
101 self.evict_if_needed();
102 Ok(kernel)
103 }
104
105 pub fn stats(&self) -> CacheStats {
107 CacheStats {
108 entries: self.map.len(),
109 hits: self.hits,
110 misses: self.misses,
111 }
112 }
113
114 fn touch(&mut self, key: &KernelCacheKey) {
116 if let Some(pos) = self.order.iter().position(|k| k == key)
117 && let Some(k) = self.order.remove(pos)
118 {
119 self.order.push_back(k);
120 }
121 }
122
123 fn evict_if_needed(&mut self) {
125 while self.map.len() > self.capacity {
126 if let Some(lru) = self.order.pop_front() {
127 self.map.remove(&lru);
128 } else {
129 break;
130 }
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use onnx_runtime_ep_api::{Result as EpResult, TensorMut, TensorView};
139
140 struct NopKernel;
142 impl Kernel for NopKernel {
143 fn execute(&self, _inputs: &[TensorView], _outputs: &mut [TensorMut]) -> EpResult<()> {
144 Ok(())
145 }
146 }
147
148 fn key(op: &str, shape: Vec<usize>) -> KernelCacheKey {
149 KernelCacheKey {
150 op_type: op.to_string(),
151 domain: String::new(),
152 opset: 26,
153 input_shapes: vec![shape],
154 input_dtypes: vec![DataType::Float32],
155 device: DeviceId::cpu(),
156 }
157 }
158
159 #[test]
160 fn miss_then_hit_same_key() {
161 let mut cache = KernelCache::new(8);
162 let k = key("Add", vec![2, 3]);
163 let mut created = 0;
164 let _ = cache
165 .get_or_create::<()>(k.clone(), || {
166 created += 1;
167 Ok(Box::new(NopKernel))
168 })
169 .unwrap();
170 let _ = cache
171 .get_or_create::<()>(k.clone(), || {
172 created += 1;
173 Ok(Box::new(NopKernel))
174 })
175 .unwrap();
176 assert_eq!(created, 1, "second dispatch must reuse the cached kernel");
177 assert_eq!(cache.stats().hits, 1);
178 assert_eq!(cache.stats().misses, 1);
179 assert_eq!(cache.stats().entries, 1);
180 }
181
182 #[test]
183 fn distinct_shapes_are_distinct_entries() {
184 let mut cache = KernelCache::new(8);
185 let _ = cache
186 .get_or_create::<()>(key("Add", vec![2, 3]), || Ok(Box::new(NopKernel)))
187 .unwrap();
188 let _ = cache
189 .get_or_create::<()>(key("Add", vec![4, 5]), || Ok(Box::new(NopKernel)))
190 .unwrap();
191 assert_eq!(cache.stats().entries, 2);
192 assert_eq!(cache.stats().misses, 2);
193 }
194
195 #[test]
196 fn lru_evicts_oldest_over_capacity() {
197 let mut cache = KernelCache::new(2);
198 let a = key("Add", vec![1]);
199 let b = key("Mul", vec![1]);
200 let c = key("Sub", vec![1]);
201 let _ = cache.get_or_create::<()>(a.clone(), || Ok(Box::new(NopKernel))).unwrap();
202 let _ = cache.get_or_create::<()>(b.clone(), || Ok(Box::new(NopKernel))).unwrap();
203 let _ = cache.get_or_create::<()>(a.clone(), || Ok(Box::new(NopKernel))).unwrap();
205 let _ = cache.get_or_create::<()>(c.clone(), || Ok(Box::new(NopKernel))).unwrap();
206 assert_eq!(cache.stats().entries, 2);
207 let mut recreated = 0;
209 let _ = cache
210 .get_or_create::<()>(b, || {
211 recreated += 1;
212 Ok(Box::new(NopKernel))
213 })
214 .unwrap();
215 assert_eq!(recreated, 1);
216 }
217}