ParallelFileReader: uncatchable fatal error when pread lands at EOF
Summary
Loading .safetensors files through the parallel file reader can abort the process with an uncatchable fatal error when a pread lands at/after EOF. The std::runtime_error thrown by ParallelFileReader crosses the C API boundary and becomes a hard crash instead of a catchable error.
Reproduction
Any multi-reader safetensors load where a reader's pread returns 0 (read at/after EOF):
// mlx/io/load.cpp (ParallelFileReader)
// load.cpp:345 static ThreadPool* thread_pool = new ThreadPool{4}; // shared singleton
// load.cpp:378 throw std::runtime_error("[read] Unable to read from file."); // sync tail read
// load.cpp:392 throw std::runtime_error("[read] Unable to read from file."); // batched futureWhen this exception is thrown from a worker thread and propagated across the C API, it is not caught and the process aborts (observed as a Swift fatal error on the consumer side).
Behavior
- Non-deterministic: the crash fires only when a reader's
preadreturns 0 at the EOF boundary (a race with the sharedThreadPool{4}work distribution). Re-running the same load usually succeeds. - The crash is a process abort, not a catchable exception — no Swift/C caller can recover.
Affected versions
- C++ MLX
ce45c52(v0.31.x) through1f8e74e(v0.32.2) — thestatic ThreadPool*shared reader and thethrow std::runtime_erroronpread == 0are present in both.
Smallest failing test
MixedPrecisionQuantLoadTests.perModuleOverridesApplied and MixedPrecisionQuantLoadTests.globalOnlyQuantizationFailsLoudly (and RerankerTests) trigger the multi-reader safetensors load and intermittently abort the test process at the C API boundary (mlx_c/transforms.cpp:73 / array.cpp:352).
Suggested fix
Catch/convert the std::runtime_error from ParallelFileReader before it crosses the C API (return an mlStatus/mlxError instead of throwing), and/or guard the EOF pread == 0 case so it does not throw on the tail read.
Source: ml-explore/mlx