-
Notifications
You must be signed in to change notification settings - Fork 150
Description
For discussion:
the mx::core (backend of mlx-swift) is written in C++ and uses C++ exceptions to signal errors. Swift has no way to catch these errors and is not exception safe -- that is if an exception were to be thrown and unwinds through stack frames implemented in Swift it would likely result in memory corruption, leaks or other poor behavior.
All (all!) mlx operations might throw. For example:
- creating a new MLXArray -- illegal shape, allocation errors
- loading weights from disk (
loadArrays()) -- I/O errors, file corruption, etc. - adding two MLXArrays (the
+operator) -- broadcast errors eval-- the prepared graph might have an eval-time error
Initially mlx-swift treated (most of) these C++ exceptions as programmer errors -- they were fatal errors that logged a message and exited the program. This is consistent with the way swift handles various programmer errors:
let a = 10
let b = 0
/// fatal error: divide by zero, exits
print(a / b)
let array = [Int]()
array.append(10)
/// fatal error: array index out of bounds, exits
print(array[20])If the value of b or the index come from user input, it is up to the program to guard against these cases if they do not want to crash.
mlx-swift follows this same principle, but the failure cases are a little more complex, such as broadcasting or indexing. Additionally the program may load weights that are downloaded from the network and these may have unexpected shapes or dtypes (kind of the case of b above). For a command line tool this behavior might be acceptable: the program crashes and prints an error and you investigate the bug/bad data. For an application used by a user other than the programmer, this isn't great -- you don't want your application to crash because the weights on a model got updated.
Note: loadArrays() is marked as throws (and always has been), though initially it could not handle some types of errors that might occur.
In a recent release the withError construct was added to give a capability to catch the C++ exceptions:
try withError {
let a = MLXArray(0 ..< 10, [2, 5])
let b = MLXArray(0 ..< 15, [3, 5])
// this will trigger a broadcast error
return a + b
}It is sort of like an autoreleasepool for the C++ exceptions. To be clear: this does not cause execution to stop and return with an error when the exception occurs -- it collects the first exception in Task local state and when the block returns it will throw that if there was an exception thrown.
This is not really the Swift style however: Swift errors are always declared and you must try and either catch or declare than the function throws.
One option would be to declare all mlx operations as throws:
let a = try MLXArray(0 ..< 10, [2, 5])
let b = try MLXArray(0 ..< 15, [3, 5])
// this will trigger a broadcast error
let c = try a + bEvery call will require a try because they can all throw. The advantage is that it looks like Swift and is explicit. The disadvantage is that it doesn't look like MLX :-)
- Could we have two variants of every call?
- note: there is a performance cost to set up the handler to catch the potential error
- is there something better than
withError? - can we make this more Swifty somehow?