use crate::tower::TokenBatch;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RuntimeIdentity {
pub name: &'static str,
pub gpu: bool,
}
#[derive(Debug)]
pub struct EmbeddingBatch {
values: Vec<f32>,
rows: usize,
dims: usize,
}
impl EmbeddingBatch {
pub fn new(values: Vec<f32>, rows: usize, dims: usize) -> Result<Self, RuntimeError> {
if rows == 0 || dims == 0 || rows.checked_mul(dims) != Some(values.len()) {
return Err(RuntimeError::Shape(
"embedding batch shape mismatch".to_owned(),
));
}
Ok(Self { values, rows, dims })
}
#[must_use]
pub fn values(&self) -> &[f32] {
&self.values
}
#[must_use]
pub(crate) fn values_mut(&mut self) -> &mut [f32] {
&mut self.values
}
#[must_use]
pub const fn rows(&self) -> usize {
self.rows
}
#[must_use]
pub const fn dims(&self) -> usize {
self.dims
}
#[must_use]
pub fn into_values(self) -> Vec<f32> {
self.values
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RuntimeError {
Tokenization(String),
Mlx(String),
InvalidCacheLimit,
MlxCache {
operation: &'static str,
status: libc::c_int,
},
MissingTensor(String),
Shape(String),
UnsupportedDtype(String),
WorkerPanicked,
}
impl std::fmt::Display for RuntimeError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tokenization(detail) => write!(formatter, "tokenization failed: {detail}"),
Self::Mlx(detail) => write!(formatter, "MLX failed: {detail}"),
Self::InvalidCacheLimit => formatter.write_str(
"ZE_MLX_CACHE_LIMIT_BYTES must be a nonnegative integer byte count fitting usize",
),
Self::MlxCache { operation, status } => {
write!(formatter, "{operation} failed with status {status}")
}
Self::MissingTensor(name) => write!(formatter, "required tensor {name} is absent"),
Self::Shape(detail) => write!(formatter, "model shape mismatch: {detail}"),
Self::UnsupportedDtype(name) => {
write!(formatter, "tensor {name} has an unsupported dtype")
}
Self::WorkerPanicked => formatter.write_str("embed worker panicked"),
}
}
}
impl std::error::Error for RuntimeError {}
pub trait ModelRuntime {
fn embed_batch(&mut self, tokens: &TokenBatch) -> Result<EmbeddingBatch, RuntimeError>;
fn warm(&mut self) -> Result<(), RuntimeError>;
fn identity(&self) -> RuntimeIdentity;
}
#[cfg(target_os = "macos")]
pub mod coreml;
pub mod mlx;
pub(crate) mod mlx_cache;
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn embedding_batches_and_runtime_errors_preserve_their_typed_contracts() {
assert!(EmbeddingBatch::new(Vec::new(), 0, 1).is_err());
assert!(EmbeddingBatch::new(vec![1.0], 1, 0).is_err());
assert!(EmbeddingBatch::new(vec![1.0], 2, 1).is_err());
let batch = EmbeddingBatch::new(vec![1.0, 2.0], 1, 2).expect("valid batch");
assert_eq!(batch.values(), [1.0, 2.0]);
assert_eq!(batch.rows(), 1);
assert_eq!(batch.dims(), 2);
for error in [
RuntimeError::Tokenization("tokens".to_owned()),
RuntimeError::Mlx("mlx".to_owned()),
RuntimeError::InvalidCacheLimit,
RuntimeError::MlxCache {
operation: "mlx_clear_cache",
status: 1,
},
RuntimeError::MissingTensor("weight".to_owned()),
RuntimeError::Shape("shape".to_owned()),
RuntimeError::UnsupportedDtype("weight".to_owned()),
RuntimeError::WorkerPanicked,
] {
assert!(!error.to_string().is_empty());
}
}
}