Pull-based error boundary: typed MLXError, native throws, poison propagation (#270) - #439
Draft
kapellirohith wants to merge 2 commits into
Draft
Pull-based error boundary: typed MLXError, native throws, poison propagation (#270)#439kapellirohith wants to merge 2 commits into
kapellirohith wants to merge 2 commits into
Conversation
Pull-based Swift side of the exception boundary (issue ml-explore#270): - MLXError struct with a typed `code` (invalidArgument / outOfRange / outOfMemory / io / runtime / unknown) mirroring mlx_error_code - checkStatus(_:) consumes the calling thread's mlx-c error slot and throws natively at the call site, closing the task-local thread hole - checkStatus(_:poisoning:) + MLXArray poison side-table carry the first error inside the value for non-throwing operator paths, ending the zombie-value cascade - ErrorBoundaryTests: broadcast mismatch, deterministic OOM via max-buffer-size allocation, corrupt safetensors -> .io, background-thread error reaching the caller's do/catch, no cross-thread bleed, first-error poison attribution Draft: depends on the mlx-c exception-boundary-fix branch (submodule bump) and the throwing sync-point changes (eval/item/asArray); the existing MLXError enum in ErrorHandler.swift is superseded pending the enum-vs-struct API decision on ml-explore#270.
…y tests pass Verified locally via `xcodebuild test -only-testing:MLXTests/ErrorBoundaryTests` (** TEST SUCCEEDED **, 6/6): - broadcast mismatch -> MLXError(.invalidArgument) via typed catch ladder - max-buffer OOM -> MLXError(.outOfMemory) via [metal::malloc] refinement - corrupt safetensors-> MLXError(.io) via [read]/[load prefix refinement - error on GCD thread reaches caller's do/catch (fatalError case fixed) - no cross-thread slot bleed (64-way concurrent graph-construction errors) - poison carries the first error through derived values to `try eval` Changes: - submodule mlx-c -> exception-boundary-fix-v0.6.0 (thread-local slot + typed catch ladder + IO refinement, rebased on the v0.6.0 pin) - vendored include/mlx/c/error.h synced with the submodule header - MLXError struct (typed .code) replaces the message-only enum; withError/ErrorBox now surface the structured type - checkStatus() pull bridge + installPullErrorBarrier() opt-in; historical push->fatalError behaviour preserved when not installed - `+ (MLXArray, MLXArray)` and add() capture status -> poison; poison propagates through derived values (first error wins) - loadArrays classifies through the slot instead of withError - ErrorBoundaryTests with a test-local throwing eval shim pending the ml-explore#270 throwing-sync-point API decision Note: concurrent GPU eval is not exercised — mlx core does not guarantee thread safety for concurrent evaluation (Metal encoder crash); isolation is proven at graph-construction time instead.
kapellirohith
marked this pull request as ready for review
July 7, 2026 17:33
kapellirohith
marked this pull request as draft
July 8, 2026 06:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Addresses #270 - full design rationale in this comment. Companion to ml-explore/mlx-c#120, which this builds on.
The current handler-based design has three gaps this PR closes:
fatalError, even insidewithError.MLXError.caught(String)cannot distinguish recoverable OOM from a programmer shape bug.MLXArrayover an empty handle; subsequent use produces misleading secondary errors.Design (pull model, opt-in)
MLXErrorstruct with typedcode(invalidArgument/outOfRange/outOfMemory/io/runtime/unknown), mirroringmlx_error_codecheckStatus(_:)consumes the calling thread's mlx-c error slot at the call site - where a real Swift frame exists - and throws natively. Because the slot is per-thread and read via the returned status, errors surface to whoever synchronizes, regardless of task contextcheckStatus(_:poisoning:), identity-keyed weak side-table); the first error rides inside the value and rethrows at the next sync pointinstallPullErrorBarrier()- opt-in no-op global handler so the push path never exits the process; without it, historical behavior (handler →fatalError) is fully preservedloadArraysclassifies through the slot (corrupt file →.io),withError/ErrorBoxnow surface the structured typeVerification
xcodebuild test -only-testing:MLXTests/ErrorBoundaryTests- 6/6 passed:testBroadcastMismatchThrows.invalidArgumentindo/catchtestOutOfMemoryClassified[metal::malloc]over max buffer size →.outOfMemorytestCorruptLoadThrowsIO.io, distinguishable from shape bugstestErrorOnBackgroundThreadReachesCallerfatalErrors today, caught in caller'sdo/catchtestNoCrossThreadBleedtestPoisonCarriesFirstErrorDraft caveats (deliberately left open for maintainer input)
Source/Cmlx/mlx-cpoints at my fork's v0.6.0-based branch (the mlx-c changes rebased onto the current pin, since mlx-cmaintargets MLX 0.31.2 vs the pinned 0.31.1). Needs Add structured thread-local error state with typed exception classification mlx-c#120 to land + a submodule bump before this can leave draft.enum MLXError→struct MLXErroris source-breaking; happy to reshape (new name + deprecation, or land in a minor bump) per your preference.try eval/item/asArray) are implemented as a test-target shim pending the API-shape decision in error handling in mlx-swift #270 - the mechanism is entirely in the library; only the public throwing overloads await consensus.eval(Metal encoder crash under 64-way eval - an existing core constraint, independent of this change); isolation is proven at graph-construction time.