1use crate::Dtype;
4use libc::strdup;
5use std::any::Any;
6use std::convert::Infallible;
7use std::ffi::NulError;
8use std::panic::Location;
9use std::sync::Once;
10use std::{cell::Cell, ffi::c_char};
11use thiserror::Error;
12
13pub type Result<T> = std::result::Result<T, Exception>;
15
16#[derive(Error, PartialEq, Debug)]
18pub enum IoError {
19 #[error("Path must point to a local file")]
21 NotFile,
22
23 #[error("Path contains invalid UTF-8")]
25 InvalidUtf8,
26
27 #[error("Path contains null bytes")]
29 NullBytes,
30
31 #[error("No file extension found")]
33 NoExtension,
34
35 #[error("Unsupported file format")]
37 UnsupportedFormat,
38
39 #[error("Unable to open file")]
41 UnableToOpenFile,
42
43 #[error("Unable to allocate memory")]
45 AllocationError,
46
47 #[error(transparent)]
49 NulError(#[from] NulError),
50
51 #[error(transparent)]
53 Unflatten(#[from] UnflattenError),
54
55 #[error(transparent)]
57 StateProjection(#[from] StateProjectionError),
58
59 #[error(transparent)]
61 Exception(#[from] Exception),
62}
63
64impl From<Infallible> for IoError {
65 fn from(_: Infallible) -> Self {
66 unreachable!()
67 }
68}
69
70impl From<RawException> for IoError {
71 #[track_caller]
72 fn from(e: RawException) -> Self {
73 let exception = Exception {
74 what: e.what,
75 location: Location::caller(),
76 };
77 Self::Exception(exception)
78 }
79}
80
81#[derive(Debug, PartialEq, Error)]
83pub enum AsSliceError {
84 #[error(
86 "array data is not contiguous row-major; call `Array::contiguous()` before borrowing it as a slice"
87 )]
88 NotContiguous,
89
90 #[error("The data pointer is null.")]
92 Null,
93
94 #[error("dtype mismatch: expected {expecting:?}, found {found:?}")]
96 DtypeMismatch {
97 expecting: Dtype,
99
100 found: Dtype,
102 },
103
104 #[error(transparent)]
106 Exception(#[from] Exception),
107}
108
109#[derive(Debug, PartialEq, Error)]
111pub enum UnflattenError {
112 #[error("Expecting next (key, value) pair, found none")]
114 ExpectingNextPair,
115
116 #[error("Invalid key")]
118 InvalidKey,
119}
120
121#[derive(Debug, PartialEq, Error)]
123pub enum OptimizerStateLoadError {
124 #[error(transparent)]
126 Io(#[from] IoError),
127
128 #[error(transparent)]
130 Unflatten(#[from] UnflattenError),
131}
132
133#[derive(Debug, PartialEq, Error)]
135pub enum StateProjectionError {
136 #[error("duplicate state key {0}")]
138 DuplicateKey(String),
139
140 #[error("state snapshot is missing key {0}")]
142 MissingKey(String),
143
144 #[error("state snapshot contains unknown keys {0:?}")]
146 UnknownKeys(Vec<String>),
147
148 #[error("required state key {0} is absent")]
150 RequiredSlotAbsent(String),
151
152 #[error("state value count mismatch: expected {expected}, found {actual}")]
154 Cardinality {
155 expected: usize,
157 actual: usize,
159 },
160
161 #[error(transparent)]
163 Exception(#[from] Exception),
164}
165
166impl From<StateProjectionError> for Exception {
167 #[track_caller]
168 fn from(error: StateProjectionError) -> Self {
169 match error {
170 StateProjectionError::Exception(error) => error,
171 error => Self::custom(error.to_string()),
172 }
173 }
174}
175
176impl From<Infallible> for OptimizerStateLoadError {
177 fn from(_: Infallible) -> Self {
178 unreachable!()
179 }
180}
181
182#[derive(Debug, Error)]
184pub enum ConversionError {
185 #[error("dtype mismatch: expected {expected:?}, found {actual:?}")]
187 DtypeMismatch {
188 expected: Dtype,
190 actual: Dtype,
192 },
193
194 #[error("scalar extraction requires one element, found {actual}")]
196 NotScalar {
197 actual: usize,
199 },
200
201 #[error(transparent)]
203 ArraySlice(#[from] AsSliceError),
204
205 #[error(transparent)]
207 Exception(#[from] Exception),
208
209 #[cfg(feature = "safetensors")]
211 #[error("The safetensors data type {0:?} is not supported.")]
212 SafeTensorDtype(safetensors::tensor::Dtype),
213
214 #[cfg(feature = "safetensors")]
216 #[error("The mlx data type {0:?} is not supported.")]
217 MlxDtype(crate::Dtype),
218
219 #[cfg(feature = "safetensors")]
221 #[error(transparent)]
222 PodCastError(#[from] bytemuck::PodCastError),
223
224 #[cfg(feature = "safetensors")]
226 #[error(transparent)]
227 SafeTensorError(#[from] safetensors::tensor::SafeTensorError),
228}
229
230pub(crate) struct RawException {
231 pub(crate) what: String,
232}
233
234#[derive(Debug, PartialEq, Error)]
236#[error("{what:?} at {location}")]
237pub struct Exception {
238 pub(crate) what: String,
239 pub(crate) location: &'static Location<'static>,
240}
241
242impl Exception {
243 pub fn what(&self) -> &str {
245 &self.what
246 }
247
248 pub fn location(&self) -> &'static Location<'static> {
254 self.location
255 }
256
257 #[track_caller]
259 pub fn custom(what: impl Into<String>) -> Self {
260 Self {
261 what: what.into(),
262 location: Location::caller(),
263 }
264 }
265}
266
267impl From<RawException> for Exception {
268 #[track_caller]
269 fn from(e: RawException) -> Self {
270 Self {
271 what: e.what,
272 location: Location::caller(),
273 }
274 }
275}
276
277impl From<&str> for Exception {
278 #[track_caller]
279 fn from(what: &str) -> Self {
280 Self {
281 what: what.to_string(),
282 location: Location::caller(),
283 }
284 }
285}
286
287impl From<Infallible> for Exception {
288 fn from(_: Infallible) -> Self {
289 unreachable!()
290 }
291}
292
293impl From<Exception> for String {
294 fn from(e: Exception) -> Self {
295 e.what
296 }
297}
298
299enum ClosureFailure {
300 Error(Exception),
301 Panic(Box<dyn Any + Send>),
302}
303
304struct LastErrorStash(Cell<*const c_char>);
307
308impl LastErrorStash {
309 fn replace(&self, ptr: *const c_char) -> *const c_char {
310 self.0.replace(ptr)
311 }
312}
313
314impl Drop for LastErrorStash {
315 fn drop(&mut self) {
316 let ptr = self.0.replace(std::ptr::null());
317 if !ptr.is_null() {
318 unsafe { libc::free(ptr as *mut libc::c_void) };
319 }
320 }
321}
322
323thread_local! {
324 static CLOSURE_ERROR: Cell<Option<ClosureFailure>> = const { Cell::new(None) };
325 static LAST_MLX_ERROR: LastErrorStash = const { LastErrorStash(Cell::new(std::ptr::null())) };
326}
327
328pub(crate) static INIT_ERR_HANDLER: ErrorHandlerRegistration = ErrorHandlerRegistration::new();
329
330pub(crate) struct ErrorHandlerRegistration(Once);
331
332impl ErrorHandlerRegistration {
333 const fn new() -> Self {
334 Self(Once::new())
335 }
336
337 pub(crate) fn call_once(&self, f: impl FnOnce()) {
338 self.0.call_once(f);
339 }
340
341 pub(crate) fn with<T>(&self, f: impl FnOnce(&Once) -> T) -> T {
342 f(&self.0)
343 }
344}
345
346#[no_mangle]
347extern "C" fn default_mlx_error_handler(msg: *const c_char, _data: *mut std::ffi::c_void) {
348 unsafe {
349 LAST_MLX_ERROR.with(|last_error| {
350 let previous = last_error.replace(strdup(msg));
353 if !previous.is_null() {
354 libc::free(previous as *mut libc::c_void);
355 }
356 });
357 }
358}
359
360pub(crate) fn setup_mlx_error_handler() {
363 unsafe {
364 mlx_sys::mlx_set_error_handler(Some(default_mlx_error_handler), std::ptr::null_mut(), None);
365 }
366}
367
368pub(crate) fn set_closure_error(err: Exception) {
369 CLOSURE_ERROR.with(|closure_error| closure_error.set(Some(ClosureFailure::Error(err))));
370}
371
372pub(crate) fn get_and_clear_closure_error() -> Option<Exception> {
373 CLOSURE_ERROR.with(|closure_error| match closure_error.take() {
374 Some(ClosureFailure::Error(err)) => Some(err),
375 Some(ClosureFailure::Panic(payload)) => resume_closure_unwind(payload),
376 None => None,
377 })
378}
379
380pub(crate) fn set_closure_panic(payload: Box<dyn Any + Send>) {
381 CLOSURE_ERROR.with(|closure_error| closure_error.set(Some(ClosureFailure::Panic(payload))));
382}
383
384pub(crate) fn resume_closure_panic() {
389 CLOSURE_ERROR.with(|closure_error| {
390 if let Some(failure) = closure_error.take() {
391 match failure {
392 ClosureFailure::Error(err) => {
393 closure_error.set(Some(ClosureFailure::Error(err)));
394 }
395 ClosureFailure::Panic(payload) => resume_closure_unwind(payload),
396 }
397 }
398 });
399}
400
401fn resume_closure_unwind(payload: Box<dyn Any + Send>) -> ! {
402 let _ = get_and_clear_last_mlx_error();
405 std::panic::resume_unwind(payload)
406}
407
408#[track_caller]
409pub(crate) fn get_and_clear_last_mlx_error() -> Option<RawException> {
410 LAST_MLX_ERROR.with(|last_error| {
411 let last_err_ptr = last_error.replace(std::ptr::null());
412 if last_err_ptr.is_null() {
413 return None;
414 }
415
416 let last_err = unsafe {
417 std::ffi::CStr::from_ptr(last_err_ptr)
418 .to_string_lossy()
419 .into_owned()
420 };
421 unsafe {
422 libc::free(last_err_ptr as *mut libc::c_void);
423 }
424
425 Some(RawException { what: last_err })
426 })
427}
428
429#[track_caller]
430pub(crate) fn exception_from_status(status: i32, operation: &str) -> Exception {
431 get_and_clear_last_mlx_error()
432 .map(Exception::from)
433 .unwrap_or_else(|| Exception::custom(format!("{operation} failed with status {status}")))
434}
435
436#[derive(Debug, Clone, PartialEq, Error)]
438pub enum CrossEntropyBuildError {
439 #[error("Label smoothing factor must be in the range [0, 1)")]
441 InvalidLabelSmoothingFactor,
442}
443
444impl From<CrossEntropyBuildError> for Exception {
445 fn from(value: CrossEntropyBuildError) -> Self {
446 Exception::custom(format!("{value}"))
447 }
448}
449
450#[derive(Debug, Clone, PartialEq, Error)]
452pub enum RmsPropBuildError {
453 #[error("alpha must be non-negative")]
455 NegativeAlpha,
456
457 #[error("epsilon must be non-negative")]
459 NegativeEpsilon,
460}
461
462#[derive(Debug, Clone, PartialEq, Error)]
464pub enum AdaDeltaBuildError {
465 #[error("rho must be non-negative")]
467 NegativeRho,
468
469 #[error("epsilon must be non-negative")]
471 NegativeEps,
472}
473
474#[derive(Debug, Clone, PartialEq, Error)]
476pub enum AdafactorBuildError {
477 #[error("Either learning rate is provided or relative step is set to true")]
479 LrIsNoneAndRelativeStepIsFalse,
480}
481
482#[derive(Debug, Clone, PartialEq, Error)]
484pub enum DropoutBuildError {
485 #[error("Dropout probability must be in the range [0, 1)")]
487 InvalidProbability,
488}
489
490#[derive(Debug, PartialEq, Error)]
492pub enum ConvBuildError {
493 #[error(
495 "groups ({groups}) must be positive and divide both input channels ({input_channels}) \
496 and output channels ({output_channels})"
497 )]
498 InvalidGroups {
499 groups: i32,
501 input_channels: i32,
503 output_channels: i32,
505 },
506
507 #[error("grouped 3D convolution is not supported (groups = {0})")]
509 GroupedConv3d(i32),
510
511 #[error(transparent)]
513 Exception(#[from] Exception),
514}
515
516impl From<ConvBuildError> for Exception {
517 #[track_caller]
518 fn from(value: ConvBuildError) -> Self {
519 match value {
520 ConvBuildError::Exception(exception) => exception,
521 other => Exception::custom(other.to_string()),
522 }
523 }
524}
525
526#[derive(Debug, PartialEq, Error)]
528pub enum MultiHeadAttentionBuildError {
529 #[error("Invalid number of heads: {0}")]
531 InvalidNumHeads(i32),
532
533 #[error(transparent)]
535 Exception(#[from] Exception),
536}
537
538#[derive(Debug, PartialEq, Error)]
540pub enum TransformerBulidError {
541 #[error("Dropout probability must be in the range [0, 1)")]
543 InvalidProbability,
544
545 #[error("Invalid number of heads: {0}")]
547 InvalidNumHeads(i32),
548
549 #[error(transparent)]
551 Exception(#[from] Exception),
552}
553
554impl From<DropoutBuildError> for TransformerBulidError {
555 fn from(e: DropoutBuildError) -> Self {
556 match e {
557 DropoutBuildError::InvalidProbability => Self::InvalidProbability,
558 }
559 }
560}
561
562impl From<MultiHeadAttentionBuildError> for TransformerBulidError {
563 fn from(e: MultiHeadAttentionBuildError) -> Self {
564 match e {
565 MultiHeadAttentionBuildError::InvalidNumHeads(n) => Self::InvalidNumHeads(n),
566 MultiHeadAttentionBuildError::Exception(e) => Self::Exception(e),
567 }
568 }
569}
570
571#[derive(Debug, Error)]
573#[error("[finfo] dtype {:?} is not inexact", .0)]
574pub struct InexactDtypeError(pub Dtype);
575
576impl From<InexactDtypeError> for Exception {
577 #[track_caller]
578 fn from(value: InexactDtypeError) -> Self {
579 Exception::custom(value.to_string())
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use crate::array;
586
587 #[test]
588 fn test_exception() {
589 let a = array!([1.0, 2.0, 3.0]);
590 let b = array!([4.0, 5.0]);
591
592 let result = a.add(&b);
593 let error = result.expect_err("Expected error");
594
595 assert!(error
598 .what()
599 .contains("Shapes (3) and (2) cannot be broadcast."))
600 }
601}