save_safetensors: write files the mmap loader can actually map - #5
Open
rcfa wants to merge 1 commit into
Open
Conversation
`tensor_from_mmap` shares the mapping only when a tensor's absolute byte offset is a multiple of its dtype width, and copies into a freshly allocated aligned buffer otherwise. `save_safetensors` guarantees the copy: it writes the header unpadded, so the data section begins at `8 + strlen(json)` — an arbitrary byte that lands on a multiple of 8 about one time in eight — and then packs tensors back to back at arbitrary relative offsets. The result is that this fork reads its own output on the slow path. On a 95 GB bundle we measured 1,775 of 2,999 tensors taking the copy path: 59.3 GB of extra anonymous memory on top of the mapping, which on a 128 GB machine filled the compressor, pushed the process into swap, and dropped generation to roughly three tokens per minute. Aligning the same shards took the compressor from 60.1 GB to 0.8 GB. Scanning 128 locally held MLX bundles, 49 were affected. Nothing about the model's behaviour reveals it — the same bytes are copied, so output is bit-identical either way. It shows up only as memory, and only once the machine is near its ceiling. This pads the header to 8 bytes and lays each tensor on an 8-byte boundary. Both are needed: an aligned relative offset is only aligned absolutely if the data section itself starts aligned. Eight covers every dtype safetensors defines, so one constant serves them all, at a cost of at most 7 bytes per tensor — about 21 KB across a 95 GB model. Header padding is sanctioned by the format rather than tolerated by it. The reference implementation carries a test asserting that readers must accept a whitespace-padded header precisely so writers may align the data section, so a padded file stays readable by every existing consumer, including older MLX and non-MLX loaders. Complements a828cb4 ("Fix mmap loading for unaligned safetensors tensors"): that made unaligned files load correctly, this stops producing them, so the fast path is reachable rather than merely survivable. Verified by extracting the layout arithmetic into a standalone program over tensors with deliberately hostile sizes (3, 10, 1 and 6 bytes, which misalign everything after them under back-to-back packing) and parsing the result with an independent reader: 6 of 6 tensors aligned, every tensor's bytes intact. The same test on the pre-change arithmetic leaves 2 of 6 misaligned, so it discriminates rather than passing vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
tensor_from_mmapshares the mapping only when a tensor's absolute byte offset is a multiple of itsdtype width, and copies into a freshly allocated aligned buffer otherwise.
save_safetensorsguarantees the copy: the header is written unpadded, so the data section begins at
8 + strlen(json)— an arbitrary byte, landing on a multiple of 8 about one time in eight — andtensors are then packed back to back at arbitrary relative offsets.
So this fork reads its own output on the slow path.
What it costs
Measured on a 95 GB bundle, 128 GB M4 Max:
The 59.3 GB is a second copy of most of the weights on top of the 95 GB mapping. Below the RAM
ceiling nobody notices; above it the machine swaps and throughput collapses.
Output is unaffected — the same bytes are copied, so greedy decoding is bit-identical before and
after. That is why this can sit undetected: it is invisible in accuracy and in any test that reads
the tensors, and shows up only as memory.
Scanning 128 locally held MLX bundles, 49 were affected, across 11 publisher namespaces — it
tracks the writer, not the packager.
The change
Pad the header to 8 bytes, and lay each tensor on an 8-byte boundary. Both are needed: an aligned
relative offset is only aligned absolutely if the data section itself starts aligned. Eight
covers every dtype safetensors defines, so one constant serves them all, at a cost of at most 7
bytes per tensor — about 21 KB across a 95 GB model.
Header padding is sanctioned by the format rather than merely tolerated. The reference
implementation carries a test for it:
so a padded file stays readable by every existing consumer, including older MLX and non-MLX loaders.
Relationship to a828cb4
This complements "Fix mmap loading for unaligned safetensors tensors",
which made unaligned files load correctly. This stops producing them, so the fast path becomes
reachable rather than merely survivable.
Verification
I did not build the full framework for this. Instead the layout arithmetic was extracted verbatim
into a standalone program and run over tensors with deliberately hostile sizes — 3, 10, 1 and 6
bytes, which misalign everything after them under back-to-back packing — with the output parsed by
an independent reader:
(8 + header_len) % 8 == 0, 6 of 6 tensors aligned, everytensor's bytes intact and distinct
3 mod 8, 2 of 6misaligned — so the test discriminates rather than passing vacuously
A build-and-run check against the real
save_safetensors/load_safetensorsround trip would beworth doing before merge; I flag that rather than imply it was done.
Context
Filed upstream as ml-explore/mlx#4439 as a feature
request — upstream never mmaps safetensors, so alignment costs it nothing there. Here the reader and
writer are in the same file and disagree, which is why the fix belongs on this branch.
Existing published files can be repaired in place without altering a single tensor byte — only
positions change. Detection needs just the first few KB of each shard: misaligned bundles start
their data section at
(8 + header_len) % 8 != 0.