Add structured thread-local error state with typed exception classification - #120
Add structured thread-local error state with typed exception classification#120kapellirohith wants to merge 2 commits into
Conversation
Adds structured, thread-local error state alongside the existing global handler so language bindings can pull a typed error at the call site instead of relying on a push callback: - mlx_error_code enum (invalid_argument / out_of_range / OOM / IO / runtime / unknown), classified from the C++ exception type with message-based refinement for Metal OOM and load/save failures - mlx_last_error_code(), mlx_last_error_message(), mlx_clear_last_error() over a thread_local slot - _mlx_error_with_code emitter; both emitters still invoke the legacy handler so mlx_set_error_handler users are unaffected - generator (python/c.py) now emits a classifying catch ladder with a trailing catch (...) closing the non-std-throw ABI hole, and routes e.what() through "%s" fixing a latent printf-format bug Generated .cpp files to be refreshed with the standard codegen step against an MLX checkout.
Mechanical application of the python/c.py CATCH_LADDER template to the checked-in generated sources (507 catch blocks) so exception types classify before regeneration against an MLX checkout. Byte-identical to generator output for these blocks.
|
This is a prototype supporting the error-handling discussion in ml-explore/mlx-swift#270 — sharing it for feedback on the approach rather than as a finished change. One note for review: the second commit mechanically applies the classifying catch ladder to the checked-in generated sources (~507 blocks) so the prototype builds. If you'd prefer, I'm happy to drop that commit entirely and let the change be regenerated through the normal codegen step — the only hand-written parts are |
|
Closing for now as i) one can install an error handler with thread-local storage; that is what we currently do for MLX Swift https://github.com/ml-explore/mlx-swift/blob/main/Source/MLX/ErrorHandler.swift#L282 ii) while the error code handling could be of interest, it currently brings to much bloat to the code |
Motivation
Design proposal discussed in ml-explore/mlx-swift#270 - see the full write-up in this comment.
Language bindings currently receive errors through a process-global push callback (
mlx_set_error_handler). A callback has no caller frame to raise a native error from, which forces bindings (e.g. mlx-swift) into out-of-band error collection, and the type of the original C++ exception is discarded - callers cannot distinguish a recoverable OOM from a shape bug. A non-std::exceptionthrow would also unwind across theextern "C"boundary today.Changes
mlx/c/error.h/error.cpp- structured, thread-local error state (pull model):mlx_error_codeenum:INVALID_ARGUMENT/OUT_OF_RANGE/OUT_OF_MEMORY/IO/RUNTIME/UNKNOWNmlx_last_error_code(),mlx_last_error_message(),mlx_clear_last_error()over athread_localslot_mlx_error_with_code(...)emitter; both emitters still invoke the legacy handler, so existing handler-based callers are unaffectedRUNTIMEerrors are refined toOUT_OF_MEMORY/IOby message inspection ([metal::malloc],[read],[load*, ...) until typed exceptions exist in mlx corepython/c.py- classifying catch ladder in the generator:std::invalid_argument→std::out_of_range→std::bad_alloc→std::exception→...(closing the non-std-throw hole)e.what()through"%s", fixing a latent printf-format bug when messages contain%Compatibility / cost
thread_localare only touched on the error pathVerification
Validated end-to-end through a mlx-swift branch that pulls this state into native Swift
throws- 6/6 boundary tests pass (broadcast → invalid-argument, max-buffer OOM, corrupt-file IO, error raised on a background thread reaching the caller, no cross-thread slot bleed, first-error propagation). Companion mlx-swift draft PR to follow.