use std::path::Path;
use std::sync::{Arc, Mutex};
use tokio::sync::{Semaphore, mpsc};
use super::admission::{AdmissionJob, AdmissionQueue, QUEUE_CAPACITY};
use super::artifact::ArtifactStore;
use super::cache::LoadedCache;
use super::runtimes::ModelRuntimes;
use crate::config::ModelsConfig;
pub struct ModelsRuntime {
pub store: Arc<ArtifactStore>,
pub admissions: AdmissionQueue,
receiver: Mutex<Option<mpsc::Receiver<AdmissionJob>>>,
pub node: String,
pub runtimes: Arc<ModelRuntimes>,
pub loaded: Arc<LoadedCache>,
pub inference_permits: Arc<Semaphore>,
pub inference_slots: usize,
}
impl ModelsRuntime {
pub fn new(config: &ModelsConfig, node: String) -> Result<Self, String> {
let cache_dir = Path::new(&config.cache_dir);
std::fs::create_dir_all(cache_dir).map_err(|e| {
format!(
"models.cache_dir '{}' could not be created: {e}",
config.cache_dir
)
})?;
let (admissions, receiver) = AdmissionQueue::new();
let inference_slots = match config.max_concurrent_inferences {
0 => std::thread::available_parallelism().map_or(1, std::num::NonZero::get),
n => n as usize,
};
Ok(Self {
store: Arc::new(ArtifactStore::new(cache_dir, config.max_cache_bytes)),
admissions,
receiver: Mutex::new(Some(receiver)),
node,
runtimes: Arc::new(ModelRuntimes::builtin(config)),
loaded: Arc::new(LoadedCache::new(config.max_loaded_bytes)),
inference_permits: Arc::new(Semaphore::new(inference_slots)),
inference_slots,
})
}
pub fn take_receiver(&self) -> Option<mpsc::Receiver<AdmissionJob>> {
self.receiver
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
}
pub fn queue_capacity(&self) -> usize {
QUEUE_CAPACITY
}
}
pub fn node_name(instance_id: &str) -> String {
if !instance_id.trim().is_empty() {
return instance_id.trim().to_string();
}
gethostname::gethostname()
.into_string()
.ok()
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_configured_instance_id_names_the_node() {
assert_eq!(node_name(" node-7 "), "node-7");
assert!(!node_name("").is_empty());
}
#[test]
fn the_receiver_is_taken_once() {
let dir = std::env::temp_dir().join(format!("orion-models-node-{}", uuid::Uuid::new_v4()));
let config = ModelsConfig {
enabled: true,
cache_dir: dir.to_string_lossy().into_owned(),
..ModelsConfig::default()
};
let runtime = ModelsRuntime::new(&config, "n".to_string()).expect("creates the dir");
assert!(dir.is_dir());
assert!(runtime.take_receiver().is_some());
assert!(runtime.take_receiver().is_none());
assert_eq!(runtime.queue_capacity(), QUEUE_CAPACITY);
assert_eq!(runtime.runtimes.names(), ["tract"]);
let (tract, device) = runtime
.runtimes
.default_for(&config, "onnx")
.expect("onnx has a default");
assert_eq!(tract.name(), "tract");
assert_eq!(device, "cpu");
assert_eq!(runtime.loaded.loaded_bytes(), 0);
assert_eq!(runtime.loaded.max_bytes(), config.max_loaded_bytes);
assert!(runtime.inference_slots >= 1);
assert_eq!(
runtime.inference_permits.available_permits(),
runtime.inference_slots
);
let four = ModelsConfig {
max_concurrent_inferences: 4,
..config
};
let runtime = ModelsRuntime::new(&four, "n".to_string()).expect("creates the dir");
assert_eq!(runtime.inference_slots, 4);
let _ = std::fs::remove_dir_all(&dir);
}
}