Skip to content

Add mmap_weights: zero-copy file-backed weight arrays - #4249

Open
beatakouchnir wants to merge 4 commits into
ml-explore:mainfrom
beatakouchnir:feature/mmap-weights
Open

Add mmap_weights: zero-copy file-backed weight arrays#4249
beatakouchnir wants to merge 4 commits into
ml-explore:mainfrom
beatakouchnir:feature/mmap-weights

Conversation

@beatakouchnir

Copy link
Copy Markdown

Proposed changes

Adds mx.mmap_weights(file, byte_offset, shape, dtype): an array whose
data is an mmap'd, page-cache-backed file region wrapped through the
existing allocator::make_buffer no-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)

  • Bit-identical to mx.load, at resident speed. gemma-4-26B with all
    270 expert tensors served as views: 132 tok/s = indistinguishable from
    resident, outputs bit-identical, through gather_qmm on quantized
    experts. The same holds unquantized: the bf16 checkpoint (53 GB) streams
    at 101% of resident decode (56.7 vs 56.0 tok/s), bit-identical.
  • Load becomes a ~1–2 s reversible mapping instead of a weights read.
    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).
  • Hardened beyond the happy path. Donation windows (view's last
    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)

  1. Whole-buffer wiring. Metal wires the entire bytesNoCopy buffer on
    first kernel use, not touched pages: a one-expert gather_qmm (3 MB
    logical) faults its full 1.2 GB stacked tensor. Page-granular laziness
    does not exist — the unit of residency is the buffer. Consequence:
    mmap_weights gives zero-copy load and buffer-granular residency
    control, NOT transparent partial-tensor streaming.
  2. The wired ceiling applies to mapped buffers. A model whose touched
    buffers exceed max_recommended_working_set_size fails with a hard
    kIOGPUCommandBufferCallbackErrorOutOfMemory 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

  • safetensors provides no alignment guarantee (real checkpoints place
    tensors at odd offsets), so element-aligned callers need a one-time
    aligned repack today. A follow-up aligning save_safetensors output
    would make future checkpoints mappable in place.
  • Accounting: mapped bytes appear in mx.get_active_memory (make_buffer
    registers size) but in NO process ledger (RSS/footprint/wired) — worth a
    doc note; it confuses first-time measurement.
  • Tests included mirror the hardening list above; the measurement harness
    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 x in the boxes that apply.

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

beatakouchnir and others added 4 commits August 12, 2026 01:15
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant