mold_server/
model_cache.rs1use std::collections::HashMap;
2use std::time::Instant;
3
4use mold_inference::InferenceEngine;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ModelResidency {
9 Gpu,
11 Unloaded,
14 Parked,
17}
18
19pub struct CachedEngine {
21 pub engine: Box<dyn InferenceEngine>,
22 pub model_name: String,
23 pub residency: ModelResidency,
24 pub last_used: Instant,
25 pub vram_bytes: u64,
27}
28
29pub struct ModelCache {
37 entries: HashMap<String, CachedEngine>,
38 lru_order: Vec<String>,
40 max_cached: usize,
42}
43
44impl ModelCache {
45 pub fn new(max_cached: usize) -> Self {
46 Self {
47 entries: HashMap::new(),
48 lru_order: Vec::new(),
49 max_cached: max_cached.max(1),
50 }
51 }
52
53 pub fn insert(
56 &mut self,
57 engine: Box<dyn InferenceEngine>,
58 vram_bytes: u64,
59 ) -> Option<Box<dyn InferenceEngine>> {
60 let name = engine.model_name().to_string();
61 let mut evicted = None;
62
63 if self.entries.len() >= self.max_cached && !self.entries.contains_key(&name) {
65 evicted = self.evict_lru();
66 }
67
68 let entry = CachedEngine {
69 model_name: name.clone(),
70 residency: if engine.is_loaded() {
71 ModelResidency::Gpu
72 } else {
73 ModelResidency::Unloaded
74 },
75 last_used: Instant::now(),
76 vram_bytes,
77 engine,
78 };
79
80 self.entries.insert(name.clone(), entry);
81 self.touch_order(&name);
82 evicted
83 }
84
85 pub fn get_mut(&mut self, model_name: &str) -> Option<&mut CachedEngine> {
87 if self.entries.contains_key(model_name) {
88 self.touch_order(model_name);
89 self.entries.get_mut(model_name)
90 } else {
91 None
92 }
93 }
94
95 pub fn contains(&self, model_name: &str) -> bool {
97 self.entries.contains_key(model_name)
98 }
99
100 pub fn remove(&mut self, model_name: &str) -> Option<Box<dyn InferenceEngine>> {
102 self.lru_order.retain(|n| n != model_name);
103 self.entries.remove(model_name).map(|e| e.engine)
104 }
105
106 pub fn unload_all(&mut self) -> Vec<String> {
109 let mut unloaded = Vec::new();
110 for entry in self.entries.values_mut() {
111 if entry.residency == ModelResidency::Gpu {
112 entry.engine.unload();
113 entry.residency = ModelResidency::Parked;
114 entry.vram_bytes = 0;
115 unloaded.push(entry.model_name.clone());
116 }
117 }
118 unloaded
119 }
120
121 pub fn unload_active(&mut self) -> Option<String> {
125 let active_name = self
126 .entries
127 .values()
128 .find(|e| e.residency == ModelResidency::Gpu)
129 .map(|e| e.model_name.clone());
130
131 if let Some(ref name) = active_name {
132 if let Some(entry) = self.entries.get_mut(name) {
133 entry.engine.unload();
134 entry.residency = ModelResidency::Parked;
135 entry.vram_bytes = 0;
136 }
137 }
138 active_name
139 }
140
141 pub fn clear(&mut self) -> Vec<Box<dyn InferenceEngine>> {
143 self.lru_order.clear();
144 self.entries.drain().map(|(_, e)| e.engine).collect()
145 }
146
147 pub fn active_vram_bytes(&self) -> u64 {
149 self.entries
150 .values()
151 .find(|e| e.residency == ModelResidency::Gpu)
152 .map(|e| e.vram_bytes)
153 .unwrap_or(0)
154 }
155
156 pub fn active_model(&self) -> Option<&str> {
158 self.entries
159 .values()
160 .find(|e| e.residency == ModelResidency::Gpu)
161 .map(|e| e.model_name.as_str())
162 }
163
164 pub fn cached_model_names(&self) -> Vec<String> {
166 self.lru_order.clone()
167 }
168
169 pub fn len(&self) -> usize {
171 self.entries.len()
172 }
173
174 pub fn is_empty(&self) -> bool {
175 self.entries.is_empty()
176 }
177
178 fn evict_lru(&mut self) -> Option<Box<dyn InferenceEngine>> {
180 if let Some(name) = self.lru_order.first().cloned() {
182 self.lru_order.remove(0);
183 return self.entries.remove(&name).map(|e| e.engine);
184 }
185 None
186 }
187
188 fn touch_order(&mut self, model_name: &str) {
190 self.lru_order.retain(|n| n != model_name);
191 self.lru_order.push(model_name.to_string());
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use anyhow::Result;
199 use mold_core::GenerateRequest;
200
201 struct MockEngine {
202 name: String,
203 loaded: bool,
204 }
205
206 impl MockEngine {
207 fn new(name: &str) -> Self {
208 Self {
209 name: name.to_string(),
210 loaded: true,
211 }
212 }
213 }
214
215 impl InferenceEngine for MockEngine {
216 fn generate(&mut self, _req: &GenerateRequest) -> Result<mold_core::GenerateResponse> {
217 unimplemented!()
218 }
219 fn model_name(&self) -> &str {
220 &self.name
221 }
222 fn is_loaded(&self) -> bool {
223 self.loaded
224 }
225 fn load(&mut self) -> Result<()> {
226 self.loaded = true;
227 Ok(())
228 }
229 fn unload(&mut self) {
230 self.loaded = false;
231 }
232 }
233
234 #[test]
235 fn insert_and_get() {
236 let mut cache = ModelCache::new(3);
237 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
238 assert!(cache.contains("model-a"));
239 assert_eq!(cache.len(), 1);
240 assert_eq!(cache.active_model(), Some("model-a"));
241 }
242
243 #[test]
244 fn lru_eviction() {
245 let mut cache = ModelCache::new(2);
246 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
247 cache.insert(Box::new(MockEngine::new("model-b")), 1000);
248 let evicted = cache.insert(Box::new(MockEngine::new("model-c")), 1000);
250 assert!(evicted.is_some());
251 assert!(!cache.contains("model-a"));
252 assert!(cache.contains("model-b"));
253 assert!(cache.contains("model-c"));
254 }
255
256 #[test]
257 fn touch_updates_lru_order() {
258 let mut cache = ModelCache::new(2);
259 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
260 cache.insert(Box::new(MockEngine::new("model-b")), 1000);
261 cache.get_mut("model-a");
263 let evicted = cache.insert(Box::new(MockEngine::new("model-c")), 1000);
264 assert!(evicted.is_some());
265 assert!(cache.contains("model-a")); assert!(!cache.contains("model-b")); assert!(cache.contains("model-c"));
268 }
269
270 #[test]
271 fn unload_active() {
272 let mut cache = ModelCache::new(3);
273 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
274 assert_eq!(cache.active_model(), Some("model-a"));
275
276 let unloaded = cache.unload_active();
277 assert_eq!(unloaded.as_deref(), Some("model-a"));
278 assert_eq!(cache.active_model(), None);
279 assert!(cache.contains("model-a"));
281 let entry = cache.get_mut("model-a").unwrap();
282 assert_eq!(entry.residency, ModelResidency::Parked);
283 }
284
285 #[test]
286 fn remove_model() {
287 let mut cache = ModelCache::new(3);
288 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
289 let removed = cache.remove("model-a");
290 assert!(removed.is_some());
291 assert!(!cache.contains("model-a"));
292 assert_eq!(cache.len(), 0);
293 }
294
295 #[test]
296 fn reinserting_same_model_does_not_evict() {
297 let mut cache = ModelCache::new(2);
298 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
299 cache.insert(Box::new(MockEngine::new("model-b")), 1000);
300 let evicted = cache.insert(Box::new(MockEngine::new("model-a")), 2000);
302 assert!(evicted.is_none());
303 assert_eq!(cache.len(), 2);
304 }
305
306 #[test]
307 fn is_empty_and_clear() {
308 let mut cache = ModelCache::new(3);
309 assert!(cache.is_empty());
310 cache.insert(Box::new(MockEngine::new("model-a")), 100);
311 assert!(!cache.is_empty());
312 let cleared = cache.clear();
313 assert_eq!(cleared.len(), 1);
314 assert!(cache.is_empty());
315 assert_eq!(cache.len(), 0);
316 }
317
318 #[test]
319 fn unload_all_parks_all_models() {
320 let mut cache = ModelCache::new(3);
321 cache.insert(Box::new(MockEngine::new("model-a")), 100);
322 cache.insert(Box::new(MockEngine::new("model-b")), 200);
323
324 let unloaded = cache.unload_all();
325 assert!(!unloaded.is_empty());
329 assert!(cache.active_model().is_none());
330 assert_eq!(cache.len(), 2);
332 }
333
334 #[test]
335 fn cached_model_names_reflects_lru_order() {
336 let mut cache = ModelCache::new(3);
337 cache.insert(Box::new(MockEngine::new("model-a")), 100);
338 cache.insert(Box::new(MockEngine::new("model-b")), 200);
339 cache.insert(Box::new(MockEngine::new("model-c")), 300);
340 assert_eq!(
342 cache.cached_model_names(),
343 vec!["model-a", "model-b", "model-c"]
344 );
345 cache.get_mut("model-a");
347 assert_eq!(
348 cache.cached_model_names(),
349 vec!["model-b", "model-c", "model-a"]
350 );
351 }
352
353 #[test]
354 fn get_mut_nonexistent_returns_none() {
355 let mut cache = ModelCache::new(3);
356 assert!(cache.get_mut("nonexistent").is_none());
357 }
358
359 #[test]
360 fn remove_nonexistent_returns_none() {
361 let mut cache = ModelCache::new(3);
362 assert!(cache.remove("nonexistent").is_none());
363 }
364
365 #[test]
366 fn unload_active_when_empty_returns_none() {
367 let mut cache = ModelCache::new(3);
368 assert!(cache.unload_active().is_none());
369 }
370
371 #[test]
372 fn max_cached_clamped_to_at_least_one() {
373 let mut cache = ModelCache::new(0);
374 cache.insert(Box::new(MockEngine::new("model-a")), 100);
375 assert_eq!(cache.len(), 1);
376 assert!(cache.contains("model-a"));
378 }
379}