model_manager.rs 23.7 KB
Newer Older
1
2
3
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

4
5
use std::{
    collections::{HashMap, HashSet},
6
    sync::Arc,
7
8
};

9
use parking_lot::{Mutex, RwLock};
10
use tokio::sync::oneshot;
11

12
13
use crate::discovery::KvWorkerMonitor;

14
use dynamo_runtime::{
15
    component::{Client, Endpoint, build_transport_type},
16
17
18
19
    discovery::DiscoverySpec,
    prelude::DistributedRuntimeProvider,
    protocols::EndpointId,
};
20
21

use crate::{
22
    kv_router::{KvRouter, KvRouterConfig, router_endpoint_id, scheduler::DefaultWorkerSelector},
23
    model_card::ModelDeploymentCard,
24
    model_type::ModelType,
25
26
27
28
29
30
31
32
    types::{
        generic::tensor::TensorStreamingEngine,
        openai::{
            chat_completions::OpenAIChatCompletionsStreamingEngine,
            completions::OpenAICompletionsStreamingEngine,
            embeddings::OpenAIEmbeddingsStreamingEngine,
        },
    },
33
};
34

35
36
37
38
39
40
41
42
/// State for prefill router activation rendezvous
enum PrefillActivationState {
    /// Decode model registered, waiting for prefill endpoint
    DecodeWaiting(oneshot::Sender<Endpoint>),
    /// Prefill endpoint arrived, waiting for decode model to register
    PrefillReady(oneshot::Receiver<Endpoint>),
}

43
44
45
46
47
48
49
50
51
#[derive(Debug, thiserror::Error)]
pub enum ModelManagerError {
    #[error("Model not found: {0}")]
    ModelNotFound(String),

    #[error("Model already exists: {0}")]
    ModelAlreadyExists(String),
}

52
53
54
55
56
57
/// Central manager for model engines, routing, and configuration.
///
/// Manages model lifecycle including engines, KV routers, prefill coordination,
/// and per-model busy thresholds for load-based request rejection.
///
/// Note: Don't implement Clone for this, put it in an Arc instead.
58
59
60
61
62
pub struct ModelManager {
    // We read a lot and write rarely, so these three are RwLock
    completion_engines: RwLock<ModelEngines<OpenAICompletionsStreamingEngine>>,
    chat_completion_engines: RwLock<ModelEngines<OpenAIChatCompletionsStreamingEngine>>,
    embeddings_engines: RwLock<ModelEngines<OpenAIEmbeddingsStreamingEngine>>,
63
    tensor_engines: RwLock<ModelEngines<TensorStreamingEngine>>,
64
65
    // Prefill models don't have engines - they're only tracked for discovery/lifecycle
    prefill_engines: RwLock<ModelEngines<()>>,
66

67
    // These are Mutex because we read and write rarely and equally
68
    cards: Mutex<HashMap<String, ModelDeploymentCard>>,
69
    kv_choosers: Mutex<HashMap<EndpointId, Arc<KvRouter>>>,
70
    prefill_router_activators: Mutex<HashMap<String, PrefillActivationState>>,
71
72
73
74
75

    /// Per-model worker monitors for dynamic KV cache load rejection.
    /// Key: model name, Value: cloneable monitor (all fields are Arc).
    /// HTTP endpoint can update thresholds via monitor.set_threshold().
    worker_monitors: RwLock<HashMap<String, KvWorkerMonitor>>,
76
77
78
79
80
81
82
83
84
85
86
87
88
89
}

impl Default for ModelManager {
    fn default() -> Self {
        Self::new()
    }
}

impl ModelManager {
    pub fn new() -> Self {
        Self {
            completion_engines: RwLock::new(ModelEngines::default()),
            chat_completion_engines: RwLock::new(ModelEngines::default()),
            embeddings_engines: RwLock::new(ModelEngines::default()),
90
            tensor_engines: RwLock::new(ModelEngines::default()),
91
            prefill_engines: RwLock::new(ModelEngines::default()),
92
            cards: Mutex::new(HashMap::new()),
93
            kv_choosers: Mutex::new(HashMap::new()),
94
            prefill_router_activators: Mutex::new(HashMap::new()),
95
            worker_monitors: RwLock::new(HashMap::new()),
96
97
98
        }
    }

99
100
101
102
103
104
105
106
107
108
109
110
111
    pub fn is_valid_checksum(
        &self,
        model_type: ModelType,
        model_name: &str,
        candidate_checksum: &str,
    ) -> Option<bool> {
        let mut results = vec![];
        for unit in model_type.units() {
            let maybe_valid_checksum = match unit {
                ModelType::Chat => self.chat_completion_engines.read().checksum(model_name),
                ModelType::Completions => self.completion_engines.read().checksum(model_name),
                ModelType::Embedding => self.embeddings_engines.read().checksum(model_name),
                ModelType::TensorBased => self.tensor_engines.read().checksum(model_name),
112
                ModelType::Prefill => self.prefill_engines.read().checksum(model_name),
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
                _ => {
                    continue;
                }
            };
            if let Some(is_valid) = maybe_valid_checksum.map(|valid_checksum| {
                tracing::debug!(
                    model_name,
                    valid_checksum,
                    candidate_checksum,
                    "is_valid_checksum: check case"
                );
                valid_checksum == candidate_checksum
            }) {
                results.push(is_valid)
            }
        }
        if results.is_empty() {
            None
        } else {
            // The checksum is valid if it is correct for all the ModelType in the bitflag.
            Some(results.into_iter().all(|x| x))
        }
    }

137
138
    pub fn get_model_cards(&self) -> Vec<ModelDeploymentCard> {
        self.cards.lock().values().cloned().collect()
139
140
    }

141
    pub fn has_model_any(&self, model: &str) -> bool {
142
143
        self.chat_completion_engines.read().contains(model)
            || self.completion_engines.read().contains(model)
144
145
    }

146
147
148
149
150
    pub fn model_display_names(&self) -> HashSet<String> {
        self.list_chat_completions_models()
            .into_iter()
            .chain(self.list_completions_models())
            .chain(self.list_embeddings_models())
151
            .chain(self.list_tensor_models())
152
            .chain(self.list_prefill_models())
153
154
155
            .collect()
    }

156
    pub fn list_chat_completions_models(&self) -> Vec<String> {
157
        self.chat_completion_engines.read().list()
158
159
160
    }

    pub fn list_completions_models(&self) -> Vec<String> {
161
        self.completion_engines.read().list()
162
163
164
    }

    pub fn list_embeddings_models(&self) -> Vec<String> {
165
        self.embeddings_engines.read().list()
166
167
    }

168
169
170
171
    pub fn list_tensor_models(&self) -> Vec<String> {
        self.tensor_engines.read().list()
    }

172
173
174
175
    pub fn list_prefill_models(&self) -> Vec<String> {
        self.prefill_engines.read().list()
    }

176
177
178
    pub fn add_completions_model(
        &self,
        model: &str,
179
        card_checksum: &str,
180
181
        engine: OpenAICompletionsStreamingEngine,
    ) -> Result<(), ModelManagerError> {
182
        let mut clients = self.completion_engines.write();
183
        clients.add(model, card_checksum, engine)
184
185
186
187
188
    }

    pub fn add_chat_completions_model(
        &self,
        model: &str,
189
        card_checksum: &str,
190
191
        engine: OpenAIChatCompletionsStreamingEngine,
    ) -> Result<(), ModelManagerError> {
192
        let mut clients = self.chat_completion_engines.write();
193
        clients.add(model, card_checksum, engine)
194
195
196
197
198
    }

    pub fn add_embeddings_model(
        &self,
        model: &str,
199
        card_checksum: &str,
200
201
        engine: OpenAIEmbeddingsStreamingEngine,
    ) -> Result<(), ModelManagerError> {
202
        let mut clients = self.embeddings_engines.write();
203
        clients.add(model, card_checksum, engine)
204
205
    }

206
207
208
    pub fn add_tensor_model(
        &self,
        model: &str,
209
        card_checksum: &str,
210
211
212
        engine: TensorStreamingEngine,
    ) -> Result<(), ModelManagerError> {
        let mut clients = self.tensor_engines.write();
213
        clients.add(model, card_checksum, engine)
214
215
    }

216
217
218
219
220
221
    pub fn add_prefill_model(
        &self,
        model: &str,
        card_checksum: &str,
    ) -> Result<(), ModelManagerError> {
        let mut clients = self.prefill_engines.write();
222
        clients.add(model, card_checksum, ())
223
224
    }

225
    pub fn remove_completions_model(&self, model: &str) -> Result<(), ModelManagerError> {
226
        let mut clients = self.completion_engines.write();
227
228
229
230
        clients.remove(model)
    }

    pub fn remove_chat_completions_model(&self, model: &str) -> Result<(), ModelManagerError> {
231
        let mut clients = self.chat_completion_engines.write();
232
233
234
235
        clients.remove(model)
    }

    pub fn remove_embeddings_model(&self, model: &str) -> Result<(), ModelManagerError> {
236
        let mut clients = self.embeddings_engines.write();
237
238
239
        clients.remove(model)
    }

240
241
242
243
244
    pub fn remove_tensor_model(&self, model: &str) -> Result<(), ModelManagerError> {
        let mut clients = self.tensor_engines.write();
        clients.remove(model)
    }

245
246
247
248
249
    pub fn remove_prefill_model(&self, model: &str) -> Result<(), ModelManagerError> {
        let mut clients = self.prefill_engines.write();
        clients.remove(model)
    }

250
    pub fn get_embeddings_engine(
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
        &self,
        model: &str,
    ) -> Result<OpenAIEmbeddingsStreamingEngine, ModelManagerError> {
        self.embeddings_engines
            .read()
            .get(model)
            .cloned()
            .ok_or(ModelManagerError::ModelNotFound(model.to_string()))
    }

    pub fn get_completions_engine(
        &self,
        model: &str,
    ) -> Result<OpenAICompletionsStreamingEngine, ModelManagerError> {
        self.completion_engines
            .read()
            .get(model)
            .cloned()
            .ok_or(ModelManagerError::ModelNotFound(model.to_string()))
    }

    pub fn get_chat_completions_engine(
        &self,
        model: &str,
    ) -> Result<OpenAIChatCompletionsStreamingEngine, ModelManagerError> {
        self.chat_completion_engines
            .read()
            .get(model)
            .cloned()
            .ok_or(ModelManagerError::ModelNotFound(model.to_string()))
    }

283
284
285
286
287
288
289
290
291
292
293
    pub fn get_tensor_engine(
        &self,
        model: &str,
    ) -> Result<TensorStreamingEngine, ModelManagerError> {
        self.tensor_engines
            .read()
            .get(model)
            .cloned()
            .ok_or(ModelManagerError::ModelNotFound(model.to_string()))
    }

294
295
296
297
298
    /// Save a ModelDeploymentCard from an instance's ModelDeploymentCard key so we can fetch it later when the key is
    /// deleted.
    pub fn save_model_card(&self, key: &str, card: ModelDeploymentCard) -> anyhow::Result<()> {
        self.cards.lock().insert(key.to_string(), card);
        Ok(())
299
300
    }

301
302
303
    /// Remove and return model card for this instance's etcd key. We do this when the instance stops.
    pub fn remove_model_card(&self, key: &str) -> Option<ModelDeploymentCard> {
        self.cards.lock().remove(key)
304
305
306
307
    }

    pub async fn kv_chooser_for(
        &self,
308
        endpoint: &Endpoint,
309
        kv_cache_block_size: u32,
310
        kv_router_config: Option<KvRouterConfig>,
311
    ) -> anyhow::Result<Arc<KvRouter>> {
312
        let endpoint_id = endpoint.id();
313

314
        if let Some(kv_chooser) = self.get_kv_chooser(&endpoint_id) {
315
316
317
            // Check if the existing router has a different block size
            if kv_chooser.block_size() != kv_cache_block_size {
                tracing::warn!(
318
                    endpoint = %endpoint_id,
319
320
                    existing_block_size = %kv_chooser.block_size(),
                    requested_block_size = %kv_cache_block_size,
321
                    "KV Router block size mismatch! Endpoint is requesting a different kv_cache_block_size than the existing router. \
322
323
324
                     This will cause routing to fail silently. Consider using the same block size or restarting the router."
                );
            }
325
326
327
            return Ok(kv_chooser);
        }

328
        let client = endpoint.client().await?;
329
330
331
332

        // Register router via discovery mechanism
        let discovery = endpoint.component().drt().discovery();
        let instance_id = discovery.instance_id();
333
        let request_plane_mode = endpoint.drt().request_plane();
334

335
        // Build transport for router endpoint based on request plane mode
336
337
        // Use KV_ROUTER_COMPONENT as the component name to distinguish from the generate endpoint's component
        let router_endpoint_id = router_endpoint_id(endpoint.id().namespace);
338
        let transport = build_transport_type(request_plane_mode, &router_endpoint_id, instance_id);
339
340
341
342
343

        let discovery_spec = DiscoverySpec::Endpoint {
            namespace: router_endpoint_id.namespace.clone(),
            component: router_endpoint_id.component.clone(),
            endpoint: router_endpoint_id.name.clone(),
344
            transport,
345
346
347
348
349
350
        };

        discovery.register(discovery_spec).await?;

        // Use instance_id (hex) as the consumer ID for NATS consumer coordination
        let consumer_id = instance_id.to_string();
351

352
        let selector = Box::new(DefaultWorkerSelector::new(kv_router_config));
353
        let chooser = KvRouter::new(
354
355
            endpoint.clone(),
            client,
356
357
            kv_cache_block_size,
            Some(selector),
358
            kv_router_config,
359
            consumer_id,
360
361
        )
        .await?;
362
363
364
        let new_kv_chooser = Arc::new(chooser);
        self.kv_choosers
            .lock()
365
            .insert(endpoint_id, new_kv_chooser.clone());
366
367
        Ok(new_kv_chooser)
    }
368

369
370
    fn get_kv_chooser(&self, id: &EndpointId) -> Option<Arc<KvRouter>> {
        self.kv_choosers.lock().get(id).cloned()
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
    }

    /// Register a prefill router for a decode model. Returns a receiver that will be
    /// activated when the corresponding prefill model is discovered.
    /// Returns None if the decode model was already registered.
    pub fn register_prefill_router(
        &self,
        model_name: String,
    ) -> Option<oneshot::Receiver<Endpoint>> {
        let mut activators = self.prefill_router_activators.lock();

        match activators.remove(&model_name) {
            Some(PrefillActivationState::PrefillReady(rx)) => {
                // Prefill endpoint already arrived - rx will immediately resolve
                tracing::debug!(
                    model_name = %model_name,
                    "Prefill endpoint already available, returning receiver with endpoint"
                );
                Some(rx)
            }
            Some(PrefillActivationState::DecodeWaiting(tx)) => {
                // Decode already registered - this shouldn't happen, restore state and return None
                tracing::error!(
                    model_name = %model_name,
                    "Decode model already registered for this prefill router"
                );
                activators.insert(model_name, PrefillActivationState::DecodeWaiting(tx));
                None
            }
            None => {
                // New registration: create tx/rx pair, store sender and return receiver
                let (tx, rx) = oneshot::channel();
                activators.insert(
                    model_name.clone(),
                    PrefillActivationState::DecodeWaiting(tx),
                );
                tracing::debug!(
                    model_name = %model_name,
                    "No prefill endpoint available yet, storing sender for future activation"
                );
                Some(rx)
            }
        }
    }

    /// Activate a prefill router by sending the endpoint through the oneshot channel.
    /// If no decode model has registered yet, stores the endpoint for future retrieval.
    pub fn activate_prefill_router(
        &self,
        model_name: &str,
        endpoint: Endpoint,
    ) -> anyhow::Result<()> {
        let mut activators = self.prefill_router_activators.lock();

        match activators.remove(model_name) {
            Some(PrefillActivationState::DecodeWaiting(sender)) => {
                // Decode model already registered
                sender.send(endpoint).map_err(|_| {
                    anyhow::anyhow!(
                        "Failed to send endpoint to prefill router activator for model: {}",
                        model_name
                    )
                })?;

                tracing::info!(
                    model_name = %model_name,
                    "Activated prefill router for already-registered decode model"
                );

                Ok(())
            }
            Some(PrefillActivationState::PrefillReady(_)) => {
                // Prefill already activated - this shouldn't happen
                anyhow::bail!("Prefill router for model {} already activated", model_name);
            }
            None => {
                // Decode model not registered yet - create pair and immediately send endpoint
                let (tx, rx) = oneshot::channel();

                tx.send(endpoint).map_err(|_| {
                    anyhow::anyhow!("Failed to send endpoint for prefill model: {}", model_name)
                })?;

                // Store the receiver for when decode model registers
                activators.insert(
                    model_name.to_string(),
                    PrefillActivationState::PrefillReady(rx),
                );

                tracing::info!(
                    model_name = %model_name,
                    "Stored prefill endpoint for future decode model registration"
                );

                Ok(())
            }
        }
468
469
    }

470
    pub fn get_model_tool_call_parser(&self, model: &str) -> Option<String> {
471
        self.cards
472
473
            .lock()
            .values()
474
475
            .find(|c| c.display_name == model)
            .and_then(|c| c.runtime_config.tool_call_parser.as_ref())
476
            .map(|parser| parser.to_string())
477
    }
478
479
480
481
482
483
484
485
486

    /// Creates parsing options with tool call parser and reasoning parser for the specified model.
    /// Currently reasoning parser is not implemented (returns None).
    pub fn get_parsing_options(&self, model: &str) -> crate::protocols::openai::ParsingOptions {
        let tool_call_parser = self.get_model_tool_call_parser(model);
        let reasoning_parser = None; // TODO: Implement reasoning parser

        crate::protocols::openai::ParsingOptions::new(tool_call_parser, reasoning_parser)
    }
487
488
489

    /// Gets or sets the busy threshold for a model via its worker monitor.
    ///
490
491
    /// Get or set the active decode blocks threshold for a model's worker monitor.
    ///
492
493
    /// This is the primary API for HTTP endpoints and external callers.
    /// The threshold (0.0 to 1.0) controls when workers are marked as "busy"
494
    /// based on KV cache block utilization.
495
496
497
498
499
500
501
502
503
    ///
    /// # Arguments
    ///
    /// * `model` - The model name
    /// * `threshold` - `Some(value)` to set, `None` to get existing
    ///
    /// # Returns
    ///
    /// The threshold value as f64, or `None` if no monitor exists for this model.
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
    pub fn active_decode_blocks_threshold(
        &self,
        model: &str,
        threshold: Option<f64>,
    ) -> Option<f64> {
        let monitors = self.worker_monitors.read();
        let monitor = monitors.get(model)?;

        match threshold {
            Some(value) => {
                monitor.set_active_decode_blocks_threshold(value);
                Some(value)
            }
            None => Some(monitor.active_decode_blocks_threshold()),
        }
    }

    /// Get or set the active prefill tokens threshold for a model's worker monitor.
    ///
    /// The threshold is a literal token count (not a percentage).
    ///
    /// # Arguments
    ///
    /// * `model` - The model name
    /// * `threshold` - `Some(value)` to set, `None` to get existing
    ///
    /// # Returns
    ///
    /// The threshold value as u64, or `None` if no monitor exists for this model.
    pub fn active_prefill_tokens_threshold(
        &self,
        model: &str,
        threshold: Option<u64>,
    ) -> Option<u64> {
538
539
540
541
542
        let monitors = self.worker_monitors.read();
        let monitor = monitors.get(model)?;

        match threshold {
            Some(value) => {
543
                monitor.set_active_prefill_tokens_threshold(value);
544
545
                Some(value)
            }
546
            None => Some(monitor.active_prefill_tokens_threshold()),
547
548
549
550
551
        }
    }

    /// Gets or creates a worker monitor for a model.
    ///
552
553
    /// If a monitor already exists, updates its thresholds and returns a clone.
    /// If no monitor exists, creates one with the given client and thresholds.
554
555
556
557
558
    ///
    /// # Arguments
    ///
    /// * `model` - The model name
    /// * `client` - The client for subscribing to KV metrics (only used if creating new)
559
560
    /// * `active_decode_blocks_threshold` - The initial/updated active decode blocks threshold value (0.0-1.0)
    /// * `active_prefill_tokens_threshold` - The initial/updated active prefill tokens threshold value (literal token count)
561
562
563
564
565
566
567
    ///
    /// # Returns
    ///
    /// A cloneable monitor that shares state with the stored instance.
    pub fn get_or_create_worker_monitor(
        &self,
        model: &str,
568
        client: Client,
569
570
        active_decode_blocks_threshold: f64,
        active_prefill_tokens_threshold: u64,
571
572
573
574
    ) -> KvWorkerMonitor {
        let mut monitors = self.worker_monitors.write();

        if let Some(existing) = monitors.get(model) {
575
576
            existing.set_active_decode_blocks_threshold(active_decode_blocks_threshold);
            existing.set_active_prefill_tokens_threshold(active_prefill_tokens_threshold);
577
578
            existing.clone()
        } else {
579
580
581
582
583
            let monitor = KvWorkerMonitor::new(
                client,
                active_decode_blocks_threshold,
                active_prefill_tokens_threshold,
            );
584
585
586
587
588
589
590
591
592
593
594
595
            monitors.insert(model.to_string(), monitor.clone());
            monitor
        }
    }

    /// Gets an existing worker monitor for a model, if one exists.
    pub fn get_worker_monitor(&self, model: &str) -> Option<KvWorkerMonitor> {
        self.worker_monitors.read().get(model).cloned()
    }

    /// Lists all models that have worker monitors (and thus busy thresholds) configured.
    ///
596
597
    /// Returns a vector of (model_name, active_decode_blocks_threshold, active_prefill_tokens_threshold) tuples.
    pub fn list_busy_thresholds(&self) -> Vec<(String, f64, u64)> {
598
599
600
        self.worker_monitors
            .read()
            .iter()
601
602
603
604
605
606
607
            .map(|(k, monitor)| {
                (
                    k.clone(),
                    monitor.active_decode_blocks_threshold(),
                    monitor.active_prefill_tokens_threshold(),
                )
            })
608
609
            .collect()
    }
610
611
612
613
614
615
}

pub struct ModelEngines<E> {
    /// Optional default model name
    default: Option<String>,
    engines: HashMap<String, E>,
616
617
618
    /// Key: Model name, value: Checksum of the ModelDeploymentCard. New instances must have the
    /// same card.
    checksums: HashMap<String, String>,
619
620
621
622
623
624
625
}

impl<E> Default for ModelEngines<E> {
    fn default() -> Self {
        Self {
            default: None,
            engines: HashMap::new(),
626
            checksums: HashMap::new(),
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
        }
    }
}

impl<E> ModelEngines<E> {
    #[allow(dead_code)]
    fn set_default(&mut self, model: &str) {
        self.default = Some(model.to_string());
    }

    #[allow(dead_code)]
    fn clear_default(&mut self) {
        self.default = None;
    }

642
    fn add(&mut self, model: &str, checksum: &str, engine: E) -> Result<(), ModelManagerError> {
643
644
645
646
        if self.engines.contains_key(model) {
            return Err(ModelManagerError::ModelAlreadyExists(model.to_string()));
        }
        self.engines.insert(model.to_string(), engine);
647
648
        self.checksums
            .insert(model.to_string(), checksum.to_string());
649
650
651
652
653
654
655
        Ok(())
    }

    fn remove(&mut self, model: &str) -> Result<(), ModelManagerError> {
        if self.engines.remove(model).is_none() {
            return Err(ModelManagerError::ModelNotFound(model.to_string()));
        }
656
        let _ = self.checksums.remove(model);
657
658
659
660
661
662
663
664
665
666
667
668
669
670
        Ok(())
    }

    fn get(&self, model: &str) -> Option<&E> {
        self.engines.get(model)
    }

    fn contains(&self, model: &str) -> bool {
        self.engines.contains_key(model)
    }

    pub fn list(&self) -> Vec<String> {
        self.engines.keys().map(|k| k.to_owned()).collect()
    }
671
672
673
674
675
676

    /// Returns a newly allocated String for called convenience. All the places I use
    /// this I need a String.
    pub fn checksum(&self, model: &str) -> Option<String> {
        self.checksums.get(model).map(|s| s.to_string())
    }
677
}