Repository navigation
Add concurrent synchronous safetensors loading across files - #462
Conversation
|
One remaining detail: mlx-swift is pinned to MLX v0.31.1, where Load::eval_cpu uses future.wait(). MLX main now uses future.get() with stream error propagation, so the next coordinated MLX/MLX-C bump should let this checkedEval barrier surface deferred reader errors without a Swift-side workaround. |
|
OK, I am trying to merge #450 today. I won't cut tags yet -- I want to get some testing in first, but that should unlock that part of it. |
The safetensors loader is lazy and a single eval of a whole checkpoint visits tensors in dictionary order, serializing reads at effectively random file offsets. Add loadArrays(urls:) and loadArraysAndMetadata(urls:), which split every file into contiguous byte-balanced ranges in file-offset order and evaluate the ranges from concurrent work items, so reads are sequential and header parsing, I/O, and copies overlap. This moves the technique from ml-explore/mlx-swift-lm#575 down into mlx-swift, per review feedback there. Measured on an M4 Pro with NVMe at ~6.1 GB/s: an 18 GB checkpoint loads 20% faster cold (4.5 to 5.6 GB/s) and a 10 GB checkpoint 5-8% faster warm; parity when the serial loader is already disk-bound.
f6ee127 to
ec555ed
Compare
|
Rebase on main. |
|
Ugh, CI won't retrigger. Well, it was clean before the rebase and I will run it locally. |
davidkoski
left a comment
There was a problem hiding this comment.
Look at my comments and see what you think -- I was reading through the mlx side and I think just the sorting by position will do anything.
I rebased to make sure it still builds & fixed the test that called a test method that no longer exists.
I am not sure about the test failure (sendableCompileCapturingMaterializedArray) -- it is something from the MaterializedArray. I can't reproduce the failure here but I will investigate.
Done, for the |
|
Found the root cause: MLX’s compilation caches are per-thread, but Swift was only clearing the current thread’s cache. If a compiled function was destroyed on another thread, its old graph survived and could be reused when a new function received the same address-derived ID. Sealed-state rebinding had the same invalidation gap. |
davidkoski
left a comment
There was a problem hiding this comment.
Change looks good. Thanks for the detective work and fix on the compile cache!!
Proposed Changes
Follow up on ml-explore/mlx-swift-lm#575, two new public functions in
MLX:They load a multi-file safetensors checkpoint (e.g. a sharded model) using several threads at once, instead of one file after another.
Why
The existing
loadArrays(url:)is lazy: arrays only materialize when evaluated, and evaluating a whole checkpoint in one go visits tensors in dictionary order which means the underlying reads jump around the file at effectively random offsets, on a single thread. For large models this leaves a lot of disk bandwidth on the table.This PR moves the technique from ml-explore/mlx-swift-lm#575 down into mlx-swift, following the review feedback there that a concurrent synchronous load function "should eventually land in mlx-swift". Doing it here means every consumer (mlx-swift-lm included) can share one implementation instead of re-deriving it.
How it works
DispatchQueue.concurrentPerform, overlapping header parsing, I/O, and buffer copies.Measured results
M4 Pro (14 cores, 24 GB), NVMe with ~6.1 GB/s ceiling:
Honest note: current mlx core already iterates safetensors in file order at the bottom of the call stack, so the serial path is close to the disk ceiling in the evicted case. The wins are concentrated where they matter most cold loads of big models, and warm loads where copies dominate.