use crate::Dtype;
use libc::strdup;
use std::any::Any;
use std::convert::Infallible;
use std::ffi::NulError;
use std::panic::Location;
use std::sync::Once;
use std::{cell::Cell, ffi::c_char};
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Exception>;
#[derive(Error, PartialEq, Debug)]
pub enum IoError {
#[error("Path must point to a local file")]
NotFile,
#[error("Path contains invalid UTF-8")]
InvalidUtf8,
#[error("Path contains null bytes")]
NullBytes,
#[error("No file extension found")]
NoExtension,
#[error("Unsupported file format")]
UnsupportedFormat,
#[error("Unable to open file")]
UnableToOpenFile,
#[error("missing module parameters: {0:?}")]
MissingParameters(Vec<String>),
#[error("unexpected module parameters: {0:?}")]
UnexpectedParameters(Vec<String>),
#[error("shape mismatch for {key}: expected {expected:?}, got {actual:?}")]
ParameterShapeMismatch {
key: String,
expected: Vec<i32>,
actual: Vec<i32>,
},
#[error("Unable to allocate memory")]
AllocationError,
#[error(transparent)]
NulError(#[from] NulError),
#[error(transparent)]
Unflatten(#[from] UnflattenError),
#[error(transparent)]
StateProjection(#[from] StateProjectionError),
#[error(transparent)]
Exception(#[from] Exception),
}
impl From<Infallible> for IoError {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
impl From<RawException> for IoError {
#[track_caller]
fn from(e: RawException) -> Self {
let exception = Exception {
what: e.what,
location: Location::caller(),
};
Self::Exception(exception)
}
}
#[derive(Debug, PartialEq, Error)]
pub enum AsSliceError {
#[error(
"array data is not contiguous row-major; call `Array::contiguous()` before borrowing it as a slice"
)]
NotContiguous,
#[error("The data pointer is null.")]
Null,
#[error("dtype mismatch: expected {expecting:?}, found {found:?}")]
DtypeMismatch {
expecting: Dtype,
found: Dtype,
},
#[error(transparent)]
Exception(#[from] Exception),
}
#[derive(Debug, PartialEq, Error)]
pub enum UnflattenError {
#[error("Expecting next (key, value) pair, found none")]
ExpectingNextPair,
#[error("Invalid key")]
InvalidKey,
}
#[derive(Debug, PartialEq, Error)]
pub enum OptimizerStateLoadError {
#[error(transparent)]
Io(#[from] IoError),
#[error(transparent)]
Unflatten(#[from] UnflattenError),
}
#[derive(Debug, PartialEq, Error)]
pub enum StateProjectionError {
#[error("duplicate state key {0}")]
DuplicateKey(String),
#[error("state snapshot is missing key {0}")]
MissingKey(String),
#[error("state snapshot contains unknown keys {0:?}")]
UnknownKeys(Vec<String>),
#[error("required state key {0} is absent")]
RequiredSlotAbsent(String),
#[error("state value count mismatch: expected {expected}, found {actual}")]
Cardinality {
expected: usize,
actual: usize,
},
#[error(transparent)]
Exception(#[from] Exception),
}
impl From<StateProjectionError> for Exception {
#[track_caller]
fn from(error: StateProjectionError) -> Self {
match error {
StateProjectionError::Exception(error) => error,
error => Self::custom(error.to_string()),
}
}
}
impl From<Infallible> for OptimizerStateLoadError {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
#[derive(Debug, Error)]
pub enum ConversionError {
#[error("dtype mismatch: expected {expected:?}, found {actual:?}")]
DtypeMismatch {
expected: Dtype,
actual: Dtype,
},
#[error("scalar extraction requires one element, found {actual}")]
NotScalar {
actual: usize,
},
#[error(transparent)]
ArraySlice(#[from] AsSliceError),
#[error(transparent)]
Exception(#[from] Exception),
#[cfg(feature = "safetensors")]
#[error("The safetensors data type {0:?} is not supported.")]
SafeTensorDtype(safetensors::tensor::Dtype),
#[cfg(feature = "safetensors")]
#[error("The mlx data type {0:?} is not supported.")]
MlxDtype(crate::Dtype),
#[cfg(feature = "safetensors")]
#[error(transparent)]
PodCastError(#[from] bytemuck::PodCastError),
#[cfg(feature = "safetensors")]
#[error(transparent)]
SafeTensorError(#[from] safetensors::tensor::SafeTensorError),
}
pub(crate) struct RawException {
pub(crate) what: String,
}
#[derive(Debug, PartialEq, Error)]
#[error("{what:?} at {location}")]
pub struct Exception {
pub(crate) what: String,
pub(crate) location: &'static Location<'static>,
}
impl Exception {
pub fn what(&self) -> &str {
&self.what
}
pub fn location(&self) -> &'static Location<'static> {
self.location
}
#[track_caller]
pub fn custom(what: impl Into<String>) -> Self {
Self {
what: what.into(),
location: Location::caller(),
}
}
}
impl From<RawException> for Exception {
#[track_caller]
fn from(e: RawException) -> Self {
Self {
what: e.what,
location: Location::caller(),
}
}
}
impl From<&str> for Exception {
#[track_caller]
fn from(what: &str) -> Self {
Self {
what: what.to_string(),
location: Location::caller(),
}
}
}
impl From<Infallible> for Exception {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
impl From<Exception> for String {
fn from(e: Exception) -> Self {
e.what
}
}
enum ClosureFailure {
Error(Exception),
Panic(Box<dyn Any + Send>),
}
struct LastErrorStash(Cell<*const c_char>);
impl LastErrorStash {
fn replace(&self, ptr: *const c_char) -> *const c_char {
self.0.replace(ptr)
}
}
impl Drop for LastErrorStash {
fn drop(&mut self) {
let ptr = self.0.replace(std::ptr::null());
if !ptr.is_null() {
unsafe { libc::free(ptr as *mut libc::c_void) };
}
}
}
thread_local! {
static CLOSURE_ERROR: Cell<Option<ClosureFailure>> = const { Cell::new(None) };
static LAST_MLX_ERROR: LastErrorStash = const { LastErrorStash(Cell::new(std::ptr::null())) };
}
pub(crate) fn ensure_mlx_error_handler() {
static INIT: Once = Once::new();
INIT.call_once(setup_mlx_error_handler);
}
#[track_caller]
pub(crate) fn infallible<T>(f: impl FnOnce() -> T) -> T {
ensure_mlx_error_handler();
let _ = get_and_clear_last_mlx_error();
let value = f();
if let Some(error) = get_and_clear_last_mlx_error() {
panic!("{}", error.what);
}
value
}
#[no_mangle]
extern "C" fn default_mlx_error_handler(msg: *const c_char, _data: *mut std::ffi::c_void) {
unsafe {
LAST_MLX_ERROR.with(|last_error| {
let previous = last_error.replace(strdup(msg));
if !previous.is_null() {
libc::free(previous as *mut libc::c_void);
}
});
}
}
fn setup_mlx_error_handler() {
unsafe {
mlx_sys::mlx_set_error_handler(Some(default_mlx_error_handler), std::ptr::null_mut(), None);
}
}
pub(crate) fn set_closure_error(err: Exception) {
CLOSURE_ERROR.with(|closure_error| closure_error.set(Some(ClosureFailure::Error(err))));
}
pub(crate) fn get_and_clear_closure_error() -> Option<Exception> {
CLOSURE_ERROR.with(|closure_error| match closure_error.take() {
Some(ClosureFailure::Error(err)) => Some(err),
Some(ClosureFailure::Panic(payload)) => resume_closure_unwind(payload),
None => None,
})
}
pub(crate) fn set_closure_panic(payload: Box<dyn Any + Send>) {
CLOSURE_ERROR.with(|closure_error| closure_error.set(Some(ClosureFailure::Panic(payload))));
}
pub(crate) fn resume_closure_panic() {
CLOSURE_ERROR.with(|closure_error| {
if let Some(failure) = closure_error.take() {
match failure {
ClosureFailure::Error(err) => {
closure_error.set(Some(ClosureFailure::Error(err)));
}
ClosureFailure::Panic(payload) => resume_closure_unwind(payload),
}
}
});
}
fn resume_closure_unwind(payload: Box<dyn Any + Send>) -> ! {
let _ = get_and_clear_last_mlx_error();
std::panic::resume_unwind(payload)
}
#[track_caller]
pub(crate) fn get_and_clear_last_mlx_error() -> Option<RawException> {
LAST_MLX_ERROR.with(|last_error| {
let last_err_ptr = last_error.replace(std::ptr::null());
if last_err_ptr.is_null() {
return None;
}
let last_err = unsafe {
std::ffi::CStr::from_ptr(last_err_ptr)
.to_string_lossy()
.into_owned()
};
unsafe {
libc::free(last_err_ptr as *mut libc::c_void);
}
Some(RawException { what: last_err })
})
}
#[track_caller]
pub(crate) fn exception_from_status(status: i32, operation: &str) -> Exception {
get_and_clear_last_mlx_error()
.map(Exception::from)
.unwrap_or_else(|| Exception::custom(format!("{operation} failed with status {status}")))
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum CrossEntropyBuildError {
#[error("Label smoothing factor must be in the range [0, 1)")]
InvalidLabelSmoothingFactor,
}
impl From<CrossEntropyBuildError> for Exception {
fn from(value: CrossEntropyBuildError) -> Self {
Exception::custom(format!("{value}"))
}
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum RmsPropBuildError {
#[error("alpha must be non-negative")]
NegativeAlpha,
#[error("epsilon must be non-negative")]
NegativeEpsilon,
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum AdaDeltaBuildError {
#[error("rho must be non-negative")]
NegativeRho,
#[error("epsilon must be non-negative")]
NegativeEps,
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum AdafactorBuildError {
#[error("Either learning rate is provided or relative step is set to true")]
LrIsNoneAndRelativeStepIsFalse,
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum DropoutBuildError {
#[error("Dropout probability must be in the range [0, 1)")]
InvalidProbability,
}
#[derive(Debug, PartialEq, Error)]
pub enum ConvBuildError {
#[error(
"groups ({groups}) must be positive and divide both input channels ({input_channels}) \
and output channels ({output_channels})"
)]
InvalidGroups {
groups: i32,
input_channels: i32,
output_channels: i32,
},
#[error("grouped 3D convolution is not supported (groups = {0})")]
GroupedConv3d(i32),
#[error(transparent)]
Exception(#[from] Exception),
}
impl From<ConvBuildError> for Exception {
#[track_caller]
fn from(value: ConvBuildError) -> Self {
match value {
ConvBuildError::Exception(exception) => exception,
other => Exception::custom(other.to_string()),
}
}
}
#[derive(Debug, PartialEq, Error)]
pub enum MultiHeadAttentionBuildError {
#[error("Invalid number of heads: {0}")]
InvalidNumHeads(i32),
#[error(transparent)]
Exception(#[from] Exception),
}
#[derive(Debug, PartialEq, Error)]
pub enum TransformerBulidError {
#[error("Dropout probability must be in the range [0, 1)")]
InvalidProbability,
#[error("Invalid number of heads: {0}")]
InvalidNumHeads(i32),
#[error(transparent)]
Exception(#[from] Exception),
}
impl From<DropoutBuildError> for TransformerBulidError {
fn from(e: DropoutBuildError) -> Self {
match e {
DropoutBuildError::InvalidProbability => Self::InvalidProbability,
}
}
}
impl From<MultiHeadAttentionBuildError> for TransformerBulidError {
fn from(e: MultiHeadAttentionBuildError) -> Self {
match e {
MultiHeadAttentionBuildError::InvalidNumHeads(n) => Self::InvalidNumHeads(n),
MultiHeadAttentionBuildError::Exception(e) => Self::Exception(e),
}
}
}
#[derive(Debug, Error)]
#[error("[finfo] dtype {:?} is not inexact", .0)]
pub struct InexactDtypeError(pub Dtype);
impl From<InexactDtypeError> for Exception {
#[track_caller]
fn from(value: InexactDtypeError) -> Self {
Exception::custom(value.to_string())
}
}
#[cfg(test)]
mod tests {
use crate::array;
#[test]
fn infallible_constructor_does_not_report_an_earlier_error() {
super::ensure_mlx_error_handler();
unsafe {
let empty = mlx_sys::mlx_array_new();
let mut value = 0;
assert_ne!(mlx_sys::mlx_array_item_int32(&mut value, empty), 0);
assert_eq!(mlx_sys::mlx_array_free(empty), 0);
}
assert_eq!(crate::Device::cpu().get_index().unwrap(), 0);
}
#[test]
fn test_exception() {
let a = array!([1.0, 2.0, 3.0]);
let b = array!([4.0, 5.0]);
let result = a.add(&b);
let error = result.expect_err("Expected error");
assert!(error
.what()
.contains("Shapes (3) and (2) cannot be broadcast."))
}
}