Add mmap_weights: zero-copy file-backed weight arrays - #4249
Open
beatakouchnir wants to merge 4 commits into
Open
Add mmap_weights: zero-copy file-backed weight arrays#4249beatakouchnir wants to merge 4 commits into
beatakouchnir wants to merge 4 commits into
Conversation
mmap_weights(file, byte_offset, shape, dtype) maps the enclosing page range PROT_READ/MAP_SHARED, wraps it with allocator::make_buffer (the existing DLPack no-copy path), and builds a row-contiguous array at the intra-page byte offset with an explicit release+munmap deleter (never the default allocator::free, which would recycle the mapping into the buffer cache). Deleter runs on Metal completion-handler threads; pure C++. Verified on Apple Silicon: bit-equal to mx.load across U32/BF16; bit-equal gather_qmm through mapped quantized weights; pages wire while any reference lives and unwire ~1s after release (msync/mincore probes); bounds and alignment errors rejected. Note: stock safetensors provides NO alignment guarantee (real checkpoints place tensors at odd byte offsets), so element-aligned callers need an aligned store; an aligned safetensors writer would fix this at the format level. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…errors Six tests in the standard suite: bit-equality across f32/bf16/f16/u32, CPU and Metal backends, ops through views, the donation window (last reference dropped before eval must never mutate the read-only mapping), 200-cycle lifecycle, and error paths (missing file, out-of-bounds, element-misaligned offset). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CPU backend runs mmap_weights bit-equal (covered in tests), so the header comment no longer scopes it to Metal. Co-Authored-By: Claude Fable 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.
Proposed changes
Adds
mx.mmap_weights(file, byte_offset, shape, dtype): an array whosedata is an mmap'd, page-cache-backed file region wrapped through the
existing
allocator::make_bufferno-copy seam (the DLPack import path),with an explicit release+munmap deleter so the mapping is never recycled
into the buffer cache.
What it does, measured (M5 Max 128 GB; MoE expert streaming testbed)
mx.load, at resident speed. gemma-4-26B with all270 expert tensors served as views: 132 tok/s = indistinguishable from
resident, outputs bit-identical, through
gather_qmmon quantizedexperts. The same holds unquantized: the bf16 checkpoint (53 GB) streams
at 101% of resident decode (56.7 vs 56.0 tok/s), bit-identical.
Cold start pays one pay-as-you-go prefill (12.85 GB faulted at ~5 GB/s);
the page cache survives process exit, so restarts and repeat CLI
invocations start near-instantly; releasing all views and re-creating
them takes 0.05 s — a working residency-control primitive for
multi-model serving (hot model at full speed, idle models at ~zero wired
cost after release + ~1 s deferred unwire).
reference dropped before eval; eager/chained/compiled/reduction) never
mutate the mapping — backing-file SHA verified. 2,512 map/use/release
cycles: zero memory drift. Epoch swaps under in-flight decode
(pipelined async_eval holding old views): crash-free, bit-identical.
Cross-family: every weight of a dense 7B (4-bit: 735 tensors; bf16: 339)
and a per-expert-layout MoE served as views, bit-identical; mixed
per-projection quantization exercised at the kernel level.
Two Metal facts reviewers should know (both measured, probes included)
first kernel use, not touched pages: a one-expert
gather_qmm(3 MBlogical) faults its full 1.2 GB stacked tensor. Page-granular laziness
does not exist — the unit of residency is the buffer. Consequence:
mmap_weightsgives zero-copy load and buffer-granular residencycontrol, NOT transparent partial-tensor streaming.
buffers exceed
max_recommended_working_set_sizefails with a hardkIOGPUCommandBufferCallbackErrorOutOfMemory mid-pass (measured at
~112 GB on a 128 GB machine; Metal churns residency at ~300 MB/s on
the way there). So the primitive's domain is models whose weights fit
under that ceiling — where it delivers resident speed with the
load/switch/restart economics above. Past the ceiling, applications
need sub-buffer residency management (we run an expert slot-cache
there; out of scope for this PR).
Notes
tensors at odd offsets), so element-aligned callers need a one-time
aligned repack today. A follow-up aligning
save_safetensorsoutputwould make future checkpoints mappable in place.
mx.get_active_memory(make_bufferregisters size) but in NO process ledger (RSS/footprint/wired) — worth a
doc note; it confuses first-time measurement.
and probes live in the linked comment on Feature Request: On-disk (out-of-core) streaming of model weights for low-RAM inference #2878.
Checklist
Put an
xin the boxes that apply.pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes