libmir-metal 0.1.0

Metal inference backend for libmir
use std::sync::{PoisonError, mpsc};

use thiserror::Error;

use crate::engine;

pub(super) type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, Error)]
#[error("{0}")]
pub(super) struct WorkerFailure(String);

impl From<Error> for WorkerFailure {
    fn from(value: Error) -> Self {
        Self(value.to_string())
    }
}

#[derive(Debug, Error)]
pub(super) enum Error {
    #[error("native MLX FFI error: {0}")]
    Engine(#[from] engine::Error),
    #[error("lock poisoned: {0}")]
    Lock(String),
    #[error("integer conversion failed: {0}")]
    Integer(#[from] std::num::TryFromIntError),
    #[cfg(test)]
    #[error("integer parsing failed: {0}")]
    ParseInteger(#[from] std::num::ParseIntError),
    #[cfg(test)]
    #[error("benchmark output failed: {0}")]
    BenchmarkOutput(#[from] std::io::Error),
    #[error("model is not loaded: {0}")]
    ModelNotLoaded(String),
    #[error("model configuration error: {0}")]
    Models(#[from] models::ModelsError),
    #[error("native model cannot prefill an empty token sequence")]
    EmptyPrompt,
    #[error("model {model} has no active session {session}")]
    Session { model: String, session: uuid::Uuid },
    #[error("native MLX has no scheduled greedy decode")]
    NoPendingDecode,
    #[error("native MLX prefix snapshot is missing prompt logits")]
    NoPrefixLogits,
    #[error("scheduled MLX token {expected} does not match requested token {actual}")]
    PendingToken { expected: u32, actual: u32 },
    #[error("invalid native MLX decode batch: {0}")]
    InvalidDecodeBatch(String),
    #[error("native MLX worker failed: {0}")]
    Worker(#[from] WorkerFailure),
    #[error("native MLX worker channel closed")]
    WorkerClosed,
    #[error("native MLX worker channel receive failed: {0}")]
    WorkerReceive(#[from] mpsc::RecvError),
    #[error("native MLX worker thread failed to start: {0}")]
    WorkerSpawn(std::io::Error),
    #[error("native MLX worker thread panicked while unloading the model")]
    WorkerJoin,
    #[cfg(test)]
    #[error("native MLX benchmark configuration error: {0}")]
    Benchmark(String),
    #[error("native MLX does not support this model: {0}")]
    UnsupportedModel(String),
}

impl<T> From<PoisonError<T>> for Error {
    fn from(value: PoisonError<T>) -> Self {
        Self::Lock(value.to_string())
    }
}

impl From<Error> for runtime::RuntimeError {
    fn from(value: Error) -> Self {
        match value {
            Error::ModelNotLoaded(id) => Self::ModelNotLoaded(id),
            Error::Models(error) => Self::ModelLoad(error.to_string()),
            Error::UnsupportedModel(error) => Self::ModelLoad(error),
            Error::Engine(error) => Self::Backend(error.to_string()),
            Error::Lock(error) => Self::Backend(error),
            Error::Integer(error) => Self::Integer(error),
            #[cfg(test)]
            Error::ParseInteger(error) => Self::Backend(error.to_string()),
            #[cfg(test)]
            Error::BenchmarkOutput(error) => Self::Backend(error.to_string()),
            #[cfg(test)]
            Error::Benchmark(error) => Self::Backend(error),
            Error::EmptyPrompt
            | Error::Session { .. }
            | Error::NoPendingDecode
            | Error::NoPrefixLogits
            | Error::PendingToken { .. }
            | Error::InvalidDecodeBatch(_)
            | Error::Worker(_)
            | Error::WorkerClosed
            | Error::WorkerReceive(_)
            | Error::WorkerSpawn(_)
            | Error::WorkerJoin => Self::Backend(value.to_string()),
        }
    }
}