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}
15
16pub struct CachedEngine {
18 pub engine: Box<dyn InferenceEngine>,
19 pub model_name: String,
20 pub residency: ModelResidency,
21 pub last_used: Instant,
22 pub vram_bytes: u64,
24}
25
26pub struct ModelCache {
34 entries: HashMap<String, CachedEngine>,
35 lru_order: Vec<String>,
37 max_cached: usize,
39}
40
41impl ModelCache {
42 pub fn new(max_cached: usize) -> Self {
43 Self {
44 entries: HashMap::new(),
45 lru_order: Vec::new(),
46 max_cached: max_cached.max(1),
47 }
48 }
49
50 pub fn insert(
53 &mut self,
54 engine: Box<dyn InferenceEngine>,
55 vram_bytes: u64,
56 ) -> Option<Box<dyn InferenceEngine>> {
57 let name = engine.model_name().to_string();
58 let mut evicted = None;
59
60 if self.entries.len() >= self.max_cached && !self.entries.contains_key(&name) {
62 evicted = self.evict_lru();
63 }
64
65 let entry = CachedEngine {
66 model_name: name.clone(),
67 residency: if engine.is_loaded() {
68 ModelResidency::Gpu
69 } else {
70 ModelResidency::Unloaded
71 },
72 last_used: Instant::now(),
73 vram_bytes,
74 engine,
75 };
76
77 self.entries.insert(name.clone(), entry);
78 self.touch_order(&name);
79 evicted
80 }
81
82 pub fn get_mut(&mut self, model_name: &str) -> Option<&mut CachedEngine> {
84 if self.entries.contains_key(model_name) {
85 self.touch_order(model_name);
86 self.entries.get_mut(model_name)
87 } else {
88 None
89 }
90 }
91
92 pub fn contains(&self, model_name: &str) -> bool {
94 self.entries.contains_key(model_name)
95 }
96
97 pub fn remove(&mut self, model_name: &str) -> Option<Box<dyn InferenceEngine>> {
99 self.lru_order.retain(|n| n != model_name);
100 self.entries.remove(model_name).map(|e| e.engine)
101 }
102
103 pub fn unload_all(&mut self) -> Vec<String> {
105 let mut unloaded = Vec::new();
106 for entry in self.entries.values_mut() {
107 if entry.residency == ModelResidency::Gpu {
108 entry.engine.unload();
109 entry.residency = ModelResidency::Unloaded;
110 entry.vram_bytes = 0;
111 unloaded.push(entry.model_name.clone());
112 }
113 }
114 unloaded
115 }
116
117 pub fn unload_active(&mut self) -> Option<String> {
120 let active_name = self
121 .entries
122 .values()
123 .find(|e| e.residency == ModelResidency::Gpu)
124 .map(|e| e.model_name.clone());
125
126 if let Some(ref name) = active_name {
127 if let Some(entry) = self.entries.get_mut(name) {
128 entry.engine.unload();
129 entry.residency = ModelResidency::Unloaded;
130 entry.vram_bytes = 0;
131 }
132 }
133 active_name
134 }
135
136 pub fn clear(&mut self) -> Vec<Box<dyn InferenceEngine>> {
138 self.lru_order.clear();
139 self.entries.drain().map(|(_, e)| e.engine).collect()
140 }
141
142 pub fn active_model(&self) -> Option<&str> {
144 self.entries
145 .values()
146 .find(|e| e.residency == ModelResidency::Gpu)
147 .map(|e| e.model_name.as_str())
148 }
149
150 pub fn cached_model_names(&self) -> Vec<String> {
152 self.lru_order.clone()
153 }
154
155 pub fn len(&self) -> usize {
157 self.entries.len()
158 }
159
160 pub fn is_empty(&self) -> bool {
161 self.entries.is_empty()
162 }
163
164 fn evict_lru(&mut self) -> Option<Box<dyn InferenceEngine>> {
166 if let Some(name) = self.lru_order.first().cloned() {
168 self.lru_order.remove(0);
169 return self.entries.remove(&name).map(|e| e.engine);
170 }
171 None
172 }
173
174 fn touch_order(&mut self, model_name: &str) {
176 self.lru_order.retain(|n| n != model_name);
177 self.lru_order.push(model_name.to_string());
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use anyhow::Result;
185 use mold_core::GenerateRequest;
186
187 struct MockEngine {
188 name: String,
189 loaded: bool,
190 }
191
192 impl MockEngine {
193 fn new(name: &str) -> Self {
194 Self {
195 name: name.to_string(),
196 loaded: true,
197 }
198 }
199 }
200
201 impl InferenceEngine for MockEngine {
202 fn generate(&mut self, _req: &GenerateRequest) -> Result<mold_core::GenerateResponse> {
203 unimplemented!()
204 }
205 fn model_name(&self) -> &str {
206 &self.name
207 }
208 fn is_loaded(&self) -> bool {
209 self.loaded
210 }
211 fn load(&mut self) -> Result<()> {
212 self.loaded = true;
213 Ok(())
214 }
215 fn unload(&mut self) {
216 self.loaded = false;
217 }
218 }
219
220 #[test]
221 fn insert_and_get() {
222 let mut cache = ModelCache::new(3);
223 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
224 assert!(cache.contains("model-a"));
225 assert_eq!(cache.len(), 1);
226 assert_eq!(cache.active_model(), Some("model-a"));
227 }
228
229 #[test]
230 fn lru_eviction() {
231 let mut cache = ModelCache::new(2);
232 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
233 cache.insert(Box::new(MockEngine::new("model-b")), 1000);
234 let evicted = cache.insert(Box::new(MockEngine::new("model-c")), 1000);
236 assert!(evicted.is_some());
237 assert!(!cache.contains("model-a"));
238 assert!(cache.contains("model-b"));
239 assert!(cache.contains("model-c"));
240 }
241
242 #[test]
243 fn touch_updates_lru_order() {
244 let mut cache = ModelCache::new(2);
245 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
246 cache.insert(Box::new(MockEngine::new("model-b")), 1000);
247 cache.get_mut("model-a");
249 let evicted = cache.insert(Box::new(MockEngine::new("model-c")), 1000);
250 assert!(evicted.is_some());
251 assert!(cache.contains("model-a")); assert!(!cache.contains("model-b")); assert!(cache.contains("model-c"));
254 }
255
256 #[test]
257 fn unload_active() {
258 let mut cache = ModelCache::new(3);
259 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
260 assert_eq!(cache.active_model(), Some("model-a"));
261
262 let unloaded = cache.unload_active();
263 assert_eq!(unloaded.as_deref(), Some("model-a"));
264 assert_eq!(cache.active_model(), None);
265 assert!(cache.contains("model-a"));
267 let entry = cache.get_mut("model-a").unwrap();
268 assert_eq!(entry.residency, ModelResidency::Unloaded);
269 }
270
271 #[test]
272 fn remove_model() {
273 let mut cache = ModelCache::new(3);
274 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
275 let removed = cache.remove("model-a");
276 assert!(removed.is_some());
277 assert!(!cache.contains("model-a"));
278 assert_eq!(cache.len(), 0);
279 }
280
281 #[test]
282 fn reinserting_same_model_does_not_evict() {
283 let mut cache = ModelCache::new(2);
284 cache.insert(Box::new(MockEngine::new("model-a")), 1000);
285 cache.insert(Box::new(MockEngine::new("model-b")), 1000);
286 let evicted = cache.insert(Box::new(MockEngine::new("model-a")), 2000);
288 assert!(evicted.is_none());
289 assert_eq!(cache.len(), 2);
290 }
291
292 #[test]
293 fn is_empty_and_clear() {
294 let mut cache = ModelCache::new(3);
295 assert!(cache.is_empty());
296 cache.insert(Box::new(MockEngine::new("model-a")), 100);
297 assert!(!cache.is_empty());
298 let cleared = cache.clear();
299 assert_eq!(cleared.len(), 1);
300 assert!(cache.is_empty());
301 assert_eq!(cache.len(), 0);
302 }
303
304 #[test]
305 fn unload_all_parks_all_models() {
306 let mut cache = ModelCache::new(3);
307 cache.insert(Box::new(MockEngine::new("model-a")), 100);
308 cache.insert(Box::new(MockEngine::new("model-b")), 200);
309
310 let unloaded = cache.unload_all();
311 assert!(!unloaded.is_empty());
315 assert!(cache.active_model().is_none());
316 assert_eq!(cache.len(), 2);
318 }
319
320 #[test]
321 fn cached_model_names_reflects_lru_order() {
322 let mut cache = ModelCache::new(3);
323 cache.insert(Box::new(MockEngine::new("model-a")), 100);
324 cache.insert(Box::new(MockEngine::new("model-b")), 200);
325 cache.insert(Box::new(MockEngine::new("model-c")), 300);
326 assert_eq!(
328 cache.cached_model_names(),
329 vec!["model-a", "model-b", "model-c"]
330 );
331 cache.get_mut("model-a");
333 assert_eq!(
334 cache.cached_model_names(),
335 vec!["model-b", "model-c", "model-a"]
336 );
337 }
338
339 #[test]
340 fn get_mut_nonexistent_returns_none() {
341 let mut cache = ModelCache::new(3);
342 assert!(cache.get_mut("nonexistent").is_none());
343 }
344
345 #[test]
346 fn remove_nonexistent_returns_none() {
347 let mut cache = ModelCache::new(3);
348 assert!(cache.remove("nonexistent").is_none());
349 }
350
351 #[test]
352 fn unload_active_when_empty_returns_none() {
353 let mut cache = ModelCache::new(3);
354 assert!(cache.unload_active().is_none());
355 }
356
357 #[test]
358 fn max_cached_clamped_to_at_least_one() {
359 let mut cache = ModelCache::new(0);
360 cache.insert(Box::new(MockEngine::new("model-a")), 100);
361 assert_eq!(cache.len(), 1);
362 assert!(cache.contains("model-a"));
364 }
365}