The border that isn't there
PyTorch was born in a world where the GPU is a separate continent. NVIDIA cards have their own VRAM, physically distinct from system RAM, connected over PCIe. Every operation that touches the GPU has to declare its citizenship — tensor.to('cuda') — and pay the customs duty of a memory copy.
Apple Silicon doesn't have that border. The M-series chips integrate the CPU, the GPU, and the Neural Engine into one piece of silicon, and they all reach into the same physical pool of RAM. There is no separate VRAM. There is no PCIe bus between processors. There is just memory, and the question of which compute unit reads from it next.
MLX is the array library written for that physical reality. The "unified memory" feature you see touted in MLX's marketing isn't a software trick — it's the framework refusing to pretend the hardware is something it isn't.
What that looks like in code
The contrast is starkest when you put two snippets side by side. PyTorch on CUDA is a series of customs declarations:
(This PyTorch snippet is for contrast — don't run it in the mlx env. Just read the ritual.)
And here is the equivalent in MLX. Notice what isn't there.
That's the entire change. No .to(device). No .cpu() on the way back out. No async copy stream to manage. The array's storage is the unified memory the GPU and CPU share, and the framework just runs the right kernels on the right compute unit.
The proof: cross-device arithmetic without a copy
Here is the experiment that made me fully internalize this. We make two arrays, one declared on CPU and one declared on GPU, and we add them together. In PyTorch on CUDA, this is an error (you have to .to() one of them first). In MLX, it just works:
The two "devices" in MLX aren't separate memory pools. They're labels on which compute unit will execute the next operation. The data is in one place the whole time.
Why this is the design center, not a feature
Most of MLX's other choices are downstream of this one fact. Lazy evaluation exists because the framework gets to decide which compute unit runs each op based on the whole graph, not on individual call sites — that decision needs the graph in hand before it can be smart. Function transforms like mx.grad and mx.vmap are easier because there's no transfer cost to schedule around. The training loop doesn't need to interleave compute and copy; it just runs.
If you keep one mental image from this entire quest, make it this one: on Apple Silicon, the array lives in a single pool of bytes that every compute unit can read. MLX is the framework that takes that fact seriously.