~14 min · journey, safetensors, mmap, page-cache, wired-memory, measured
Level 0Spec-Sheet Skimmer
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"A model is loaded when its pages are wired. Everything before that is a promise."
Three States of the Same Bytes
A checkpoint on disk is a safetensors file: a JSON header naming every tensor and its byte range, then the raw bytes. Loading it on a Mac is not a copy into a different memory — there is no different memory — but it is three distinct states, and watching them is the clearest way to see unified memory from the kernel's side. Mapped: MLX memory-maps the file, so the tensors exist as addresses without any bytes having moved; if the file is in the page cache (a warm load) those addresses already point at pages in the pool. Touched: the first forward pass reads every weight, faulting in any page not yet resident — from the page cache in nanoseconds, from the SSD in the memory track's cliff-sized time if cold. Wired: pages the GPU is using are pinned so the kernel cannot evict them; this is the memory the recommended working set is talking about, and vm_stat reports it directly.
Measured on office
The code block snapshots vm_stat before loading the 27B, after load(), and after the first forward pass (a warm load; the file was in the page cache):
Moment
Wired
Active
Free
What happened
Evidence
before
29.28 GiB
199.50 GiB
79.51 GiB
the machine's normal state: engines, caches, everything else
measured, office, 2026-09-15
after load() — 3.71 s
27.02 GiB
213.25 GiB
52.91 GiB
mapped: the file's pages counted as active, nothing wired for the GPU yet
measured
after first forward — 1.27 s
43.41 GiB
198.31 GiB
51.60 GiB
touched and wired: +16.4 GiB wired, the model's 14.1 GiB (MLX's own count) plus working buffers
measured
Read the wired column: it barely moves at load() (it dips by two gigabytes, something else being released) and jumps by the model's size at the first forward. That is lazy loading made visible — MLX's load returns in under four seconds because it has promised the bytes, not read them; the first token pays for the reading. It is also why the lab script runs a warm-up generation before timing anything, and why a model's "load time" in any runtime is really two numbers.
Cold Loads, and What the Pool Does Not Save You From
Nothing above touched the SSD, because office had read the file earlier in the day and 512 GB of memory keeps a lot of page cache. A cold load is the same three states with a disk read in the middle: 16 GB at the Air's measured 2.85 GB/s is about six seconds, and at a Studio's SSD a couple. Unified memory removes the copy from host memory to device memory; it does not remove the read from disk to memory, and it does not make the wired pages free — they are the pages the memory track's swap cliff is about, and the GPU track's claimant discipline is about leaving enough of the pool unwired for everyone else.
Code
file_to_pages.py — vm_stat before load, after lazy load, after the first forward·python
#!/usr/bin/env python3
"""Watch a checkpoint become memory. vm_stat before, after load (lazy: mapped,
not yet touched), and after the first forward pass (pages touched and wired
for the GPU). Sizes in GiB from 16 KB pages."""
import subprocess, time
import mlx.core as mx
from mlx_lm import load
PAGE = 16384
def vm() -> dict:
out = subprocess.run(["vm_stat"], capture_output=True, text=True).stdout
return {line.split(":")[0].strip(): int(line.split(":")[1].strip().rstrip("."))
for line in out.splitlines()[1:] if ":" in line}
def gib(pages: int) -> float:
return pages * PAGE / 2**30
before = vm()
t = time.perf_counter(); model, tok = load("mlx-community/Qwen3.5-27B-4bit"); t_load = time.perf_counter() - t
after_load = vm()
prompt = mx.array(tok.apply_chat_template([{"role": "user", "content": "hi"}], add_generation_prompt=True))[None]
t = time.perf_counter(); mx.eval(model(prompt)); t_first = time.perf_counter() - t
after_first = vm()
for name, snap in (("before", before), ("after load()", after_load), ("after first forward", after_first)):
print(f"{name:20} wired {gib(snap['Pages wired down']):7.2f} GiB active {gib(snap['Pages active']):7.2f} GiB free {gib(snap['Pages free']):7.2f} GiB")
print(f"\nload() {t_load:.2f}s (lazy: mapped, not read) first forward {t_first:.2f}s (pages touched, wired)")
print(f"MLX active memory after first forward: {mx.get_active_memory()/2**30:.2f} GiB")
# office, M3 Ultra, 2026-09-15 (warm):
# before wired 29.28 active 199.50 free 79.51
# after load() wired 27.02 active 213.25 free 52.91 load() 3.71s
# after first forward wired 43.41 active 198.31 free 51.60 first forward 1.27s; MLX active 14.09 GiB
The safetensors header is plain JSON at the front of the file·bash
# first 8 bytes: header length; then the JSON header naming every tensor and its byte range
f=~/.cache/huggingface/hub/models--mlx-community--Qwen3.5-27B-4bit/snapshots/*/model-00001-of-*.safetensors
python3 - "$f" <<'PY'
import json, struct, sys, glob
path = glob.glob(sys.argv[1])[0]
with open(path, "rb") as fh:
n = struct.unpack("<Q", fh.read(8))[0]
header = json.loads(fh.read(n))
print(f"{len(header)} tensors in this shard; header {n/1024:.0f} KB")
name, meta = next((k, v) for k, v in header.items() if k != "__metadata__")
print(name, meta["dtype"], meta["shape"], "bytes", meta["data_offsets"][1] - meta["data_offsets"][0])
PY
# office, 2026-09-15: 848 tensors in this shard; header 102 KB
# language_model.model.embed_tokens.biases BF16 [248320, 80] bytes 39731200
Run file_to_pages.py on your Mac with a model you have (and a size your pool holds). Record the three wired figures and the two times. Then reboot or drop the file from cache (copy a file larger than RAM, or simply wait a day) and run it again cold: how long did the first forward take, and does it match your SSD's measured bandwidth from the memory track?
Hint
Cold first-forward time ≈ file bytes ÷ SSD GB/s, plus the warm figure. If it is much longer, the read was random rather than sequential (many small tensors) or the page cache was fighting other memory; if it is shorter, the file was not as cold as you thought.
Progress
Progress is local-only — sign in to sync across devices.