ml-explore/mlx:Apple 官方开源的 Apple Silicon 数组框架,统一内存 + 动态图 + 多后端
posts posts 2026-06-17T15:03:26+08:00MLX 是 Apple 机器学习研究团队开源的数组框架,深度结合 Apple Silicon 统一内存特性,提供 Python/C++/C/Swift API。本文系统拆解其 lazy 计算图、Primitive 抽象、Metal/CPU/CUDA 后端与分布式运行时。技术笔记MLX, Apple Silicon, 数组框架, 统一内存, 动态图ml-explore/mlx:Apple 官方开源的 Apple Silicon 数组框架,统一内存 + 动态图 + 多后端
类型:原理拆解 + 架构分析(multi-backend, lazy graph, unified memory) 适合读者:在 Apple Silicon 上做 ML 研究/部署的工程师;想理解 MLX 与 PyTorch/JAX 在设计哲学上差异的框架作者;正在评估"是否把训练/推理搬上 Mac"的团队技术负责人。 本文立场:以
ml-explore/mlx仓库main分支 2026-06-17 当天状态为准,不替读者对"是否弃用 PyTorch"下结论。
目录
- 1. 一句话判断
- 2. 系统地图:组件拆分与职责边界
- 3. 关键机制:lazy + 动态图 + 统一内存
- 4. 任务流案例:手写一个 matmul + softmax,看图怎么落
- 5. 真实工作负载:benchmarks 解读
- 6. 构建与上手:最少必要配置
- 7. 与其他框架的边界
- 8. 适用边界与采用顺序
- 9. 自测题与进阶路径
- 附录 A:关键文件索引
- 附录 B:常用命令
- 附录 C:参考链接
学习目标
读完本文后,你应当能够:
- 说出 MLX 与 PyTorch/JAX 在执行模型上的核心差异:lazy graph、统一内存、不暴露 device API 这三件事分别解决了什么问题。
- 在
mlx/仓库里定位到array.h、compile.cpp、primitives/、backend/metal/、distributed/这五个关键目录,并解释各自的职责边界。 - 跟着第 4 节的任务流案例,复述一次
mx.softmax(mx.matmul(x, w))从构造到print触发eval()的完整路径,包括 status 状态机的转换。 - 看 MLX benchmark 表格时,能区分"测的是什么"“数字反映哪部分系统"“不能推出什么"这三件事,不会把"同硬件 2× 提升"误读成"比 A100 快”。
- 根据自己的硬件(Apple silicon / CUDA GPU / Intel Mac)和场景(研究 / 推理 / 训练 / 嵌入),判断是否该选 MLX,并知道从哪个
mlx-examplesdemo 开始验证。
1. 一句话判断
ml-explore/mlx(下文简称 MLX)是 Apple 机器学习研究团队自研、面向 Apple silicon 的数组框架(array framework)。Python 写起来像 NumPy 只是表面相似,真正让 MLX 与 PyTorch/JAX 区分开的是下面三件事同时成立:
- 统一内存(unified memory)模型——数组本身没有"我在 CPU 还是 GPU"这种属性,CPU/GPU 后端共享同一份内存,无需显式
.to('mps')。 - 动态构建 lazy 计算图——改变函数参数的 shape 不会触发重编译(trace),调试体验接近 Eager。
- 多后端 Metal/CPU/CUDA 一套 IR——同一个图可以分发到 Apple GPU、x86_64 CPU、CUDA GPU 上跑,调度器决定节点去哪。
仓库当前状态(截至 2026-06-17):
| 指标 | 数值 |
|---|---|
| 仓库 | ml-explore/mlx |
| Stars | 27,087 |
| Forks | 1,918 |
| 主语言 | C++(含 C/C++/Python bindings) |
| License | MIT |
| 默认分支 | main |
| 最新推送 | 2026-06-17 06:00 UTC(仍在活跃维护) |
| 构建系统 | CMake 3.25+(C++20) |
| 同源仓库 | ml-explore/mlx-swift(Swift API)、ml-explore/mlx-c(C API)、ml-explore/mlx-examples(LLaMA/Whisper/SD 示例) |
本文不展开
mlx-swift/mlx-c的 Swift/C 细节,只在「多语言 API」一节对齐其与主仓的边界。
2. 系统地图:组件拆分与职责边界
MLX 的核心实现完全是 C++20,整个 C++ 核心只通过 pybind11 / nanobind 暴露给 Python。mlx/ 目录下源码超过 200 个文件,按职责切分成下面五层。
┌────────────────────────────────────────┐
│ Python / C++ / C / Swift API│
│ (numpy-style 数组 + mlx.nn + mlx.optimizers)│
└────────────────────┬───────────────────┘
│ Array / Primitive / Stream
┌────────────────────▼───────────────────┐
│ 核心层 (core) │
│ array + ArrayDesc + Primitive + Compiled│
│ graph + eval + jit + compile │
└──────────┬─────────────┬─────────────┘
│ │
┌──────────────▼─┐ ┌──────▼──────────────┐
│ CPU Backend │ │ Metal / CUDA Backend│
│ accelerate + │ │ (Metal kernels + JIT│
│ cblas / SIMD │ │ compile pipeline) │
└────────────────┘ └──────────────────────┘
│ │
└──────┬──────┘
▼
┌──────────────────────────────────┐
│ Distributed runtime (RPC + │
│ Ring / STREAM-based allreduce) │
└──────────────────────────────────┘2.1 五层职责对照
| 层 | 关键文件 | 职责 | 不负责什么 |
|---|---|---|---|
| API 层 | python/src/array.cpp、python/src/primitives.cpp | 把 array、primitive、compile 暴露成 NumPy 风格 | 不直接管内存、不直接编译 kernel |
| 核心层 | mlx/array.h、mlx/array.cpp、mlx/compile.cpp、mlx/primitives/*.h | 维护 lazy 图、把 Primitive 落成 Compiled、调度 eval | 不关心底层走 Metal 还是 CPU(交给 backend) |
| CPU Backend | mlx/backend/accelerate/、mlx/backend/no_math/、mlx/backend/cpu/ | BLAS / Accelerate.framework / SIMD 加速 | 不知道图结构,只接收落地的算子调用 |
| Metal / CUDA Backend | mlx/backend/metal/、mlx/backend/metal/kernels/、mlx/backend/cuda/ | 生成 Metal MSL 源码、JIT 编译到 GPU pipeline | 决定算子语义(语义由 Primitive 给出) |
| 分布式运行时 | mlx/distributed/(ring_allreduce、all_gather、ring_sum 等) | 跨 device / 跨 host 的 allreduce、all_gather | 不参与单卡上的算子执行 |
2.2 array 是什么:一个图节点
mlx/array.h 顶部的注释一句话点透了 MLX 的设计核心:
/* An array is really a node in a graph. It contains a shared ArrayDesc object */
class MLX_API array {
/* An array is really a node in a graph. It contains a shared ArrayDesc
* object */
public:
template <typename T>
explicit array(T val, Dtype dtype = TypeToDtype<T>());
template <typename It>
explicit array(It data, Shape shape, Dtype dtype = ...);
void eval(); // 触发落地
std::vector<array> outputs() const; // 图拓扑遍历
void detach(); // 从图上摘下,置为常量
private:
std::shared_ptr<ArrayDesc> array_desc_;
};
struct ArrayDesc {
Shape shape;
Strides strides;
size_t size;
Dtype dtype;
std::shared_ptr<Primitive> primitive; // 算子 + inputs
std::vector<array> inputs; // 输入节点
std::vector<array> siblings; // 兄弟节点
uint32_t position; // 在 primitive outputs 中的位置
// ... status / event / shared Data buffer
};关键事实:MLX 的 array 本身没有 buffer。ArrayDesc 里 std::shared_ptr<Data> data 那一栏,只有在 eval() 真正把图落地、或者这个数组是用户手动 array(数据, shape) 构造的时候才存在。具体到两种情况:
- 写
a + b时,结果是一个新的图节点,里面只存"我等于Add算子、输入是a和b",没有任何数值。 - 调用
a.eval()、print(a)、np.array(a)、或者把a喂进 backend 的eval_cpu/eval_gpu时,才真正把这条路径上的节点求值。
这跟 PyTorch 的 “Eager 模式 + .item() 才同步"看起来像,但内核不一样:PyTorch 是每个 op 同步落地的,MLX 是攒到边界一起落。PyTorch 的 compile 才接近 MLX 的默认行为。
3. 关键机制:lazy + 动态图 + 统一内存
MLX 反复在 README 强调的 5 个特性,其实可以收束到下面三组工程机制。
3.1 Lazy evaluation + dynamic graph
Lazy 性质由 array_desc_->status 这个枚举维护:
enum Status {
// The output of a computation which has not been scheduled.
unscheduled,
// The array's eval_* function has been run, but the computation is not
// necessarily complete. The array will have memory allocated and if it is
// not a tracer then it will be detached from the graph
evaluated,
// If the array is the output of a computation then the computation
// is complete. Constant arrays are always available (e.g. array({1, 2, 3}))
available
};调度入口在 mlx/array.cpp 的 array::eval():
void array::eval() {
// 1. 把图上所有 unscheduled 节点拓扑排序
// 2. 按 device (cpu / gpu) 切 stream
// 3. 调 backend 的 make_compiled_XXX 把 primitive 编译成 kernel
// 4. 丢到 Stream 队列等执行
mlx::core::eval({*this});
}动态图的好处显式写在 mlx/array.h 的 is_tracer 注释里——Tracer 数组不会被求值,可以被 compile 拉走做"占位符”。这让 MLX 的 compile(fn) 走的是追踪而不是 static graph capture,所以 fn 里的 if shape[0] > 1024 这种 Python 分支可以工作;PyTorch 2.x 的 torch.compile 在这种分支上会重 trace,MLX 是把分支判定延后到真正的 eval 时刻。
3.2 Unified memory:跨 device 无需搬运
这是 MLX 和 PyTorch MPS 后端最大的语义差异。看 mlx/array.h::data() 的注释:
// Return a raw pointer to the arrays data. This function may do a copy if
// the underlying buffer is not accessible on the CPU. When accessing the
// data for GPU kernels, be sure to use the correct method / function for the
// given backend to access the GPU pointer.
T* data() {
return reinterpret_cast<T*>(
(static_cast<char*>(buffer().raw_ptr()) + array_desc_->offset));
}关键在最后那段注释——data() 在 GPU buffer 上可能做一次 copy。但 MLX 路径上 CPU 和 GPU 共享的是同一份虚拟地址(macOS 的 unified memory、CUDA 的 cudaMallocManaged),backend 在执行时直接拿 GPU 指针访问,不会真的搬运。对外的 Python API 完全不暴露"我在哪个 device”:
import mlx.core as mx
a = mx.array([1.0, 2.0, 3.0]) # 默认 device 是哪都行
b = mx.array([4.0, 5.0, 6.0])
c = a + b # 没有 .to('mps')
print(c) # 隐式触发 evalPyTorch 等价代码需要 a.to('mps'); b.to('mps'),且 MPS/CPU 之间 .to() 是同步搬运——这在 batch 切分、gradient accumulation 这种场景下要写一堆 device plumbing。MLX 的"无 device API" 抹平了这层。
3.3 多 backend:一套 Primitive,三套执行路径
mlx/primitives/ 目录下每个算子是一个类,继承 Primitive 基类。Backend 通过 def_<name>(input, stream, ...) 这个注册机制插桩:
// mlx/backend/metal/primitives/Addition.cpp
void Add::eval(const array& a, const array& b, array& out) {
// 1. 准备 Metal buffer (desc.data 是 shared_ptr 跨 backend 共享)
// 2. 拼 MSL kernel 源码
// 3. 编入 Metal pipeline
// 4. 提交到 Stream
}加法、matmul、softmax、RoPE、RMSNorm 这些核心 op 在三个 backend 下都有一份实现:
| Backend | 关键目录 | 加速方式 |
|---|---|---|
| CPU | mlx/backend/accelerate/、mlx/backend/cpu/ | Accelerate.framework(macOS)、BLAS、SIMD |
| Metal | mlx/backend/metal/kernels/*.metal | JIT 编译 MSL 源码为 Metal pipeline |
| CUDA | mlx/backend/cuda/ | 自有 CUDA kernel + cuBLAS |
CMake 选项控制开关(CMakeLists.txt):
option(MLX_BUILD_METAL "Build metal backend" ON)
option(MLX_BUILD_CPU "Build cpu backend" ON)
option(MLX_BUILD_CUDA "Build cuda backend" OFF)
option(MLX_METAL_JIT "Use JIT compilation for Metal kernels" OFF)MLX_METAL_JIT=OFF(默认)时,Metal kernel 在 build 期预编译成 metallib;打开后会走运行时 JIT,方便热改。MLX_BUILD_CUDA=OFF 是为了在 macOS 上不依赖 CUDA toolchain。
3.4 分布式:mlx.distributed 不是 NCCL
mlx/distributed/ 提供了 all_sum、all_gather、all_reduce 等 collective,但实现走的是 socket/TCP + Ring 算法,没有依赖 NCCL。mlx-examples/llms/llama 里的多机训练 demo 走的是这条路径。
Ring 的实现在 mlx/distributed/ring/ring.cpp,核心思想是:
- 把 tensor 沿 chunk 维度切 N 份(N = world size)。
- 每个节点持有自己那份 + 正在发送/接收的"邻居份"。
- 走 N-1 步 ring,每步送一份并累加,结束时每人都拿到完整 sum。
对单机多 GPU 不友好(这部分仍走 backend 自己的 stream),主要解决多机多卡 Apple silicon(Mac Studio 集群)或多机 Linux+CUDA 的场景。MLX_ENABLE_X64_MAC=OFF 显式禁掉 Intel Mac build(截至撰写时,该选项在 CMakeLists.txt 顶层定义,行号可能随版本变动,建议用 grep -n MLX_ENABLE_X64_MAC CMakeLists.txt 当场核对),因为 Apple silicon 才是首选目标。
4. 任务流案例:手写一个 matmul + softmax,看图怎么落
下面这段 MLX 代码看着像 PyTorch 演示,但执行路径完全不一样。
import mlx.core as mx
def layer(x, w):
return mx.softmax(mx.matmul(x, w) * 0.125, axis=-1)
x = mx.random.normal((4, 16))
w = mx.random.normal((16, 8))
y = layer(x, w)
print(y)执行过程:
x、w构造:status=available(用户给的 buffer,shared_ptr 直接挂上)。is_tracer=false。mx.matmul(x, w):返回新 array,ArrayDesc.primitive = Matmul(x, w),status=unscheduled,没有数值。... * 0.125:再返回新 array,primitive = Multiply(matmul_x_w, 0.125),status=unscheduled。此刻这条链上没有任何字节被算出来。mx.softmax(..., axis=-1):又一层包装,primitive = Softmax(...)。print(y):Python 这边__repr__触发y.eval():- 拓扑排序找出
y的祖先链:y → softmax → mul → matmul → x, w。 - 切 stream(默认
default_device()决定的 backend)。 - backend 把
Matmul编译成对应 kernel(CPU → Accelerate SGEMM;Metal → MPS 矩阵乘;CUDA → cuBLAS GEMM)。 - 中间结果
mul的 buffer 在需要时再 lazy alloc。 - 算到
y真正有数值后,__repr__把内容打到 stdout。
- 拓扑排序找出
- 如果只调
y.eval()不取数:图被求值一次,结果可以就地复用——下次访问y不需要重算。 - 如果把
layer用mx.compile(layer)包一下:compile第一次调用会 tracelayer、把x、w换成 tracer、跑一次得到一个Compiled对象。下次再用相同 shape 调用直接走编译产物;如果 shape 变了,MLX 因为是 dynamic graph,不会重 trace,直接把 trace 时的 tracer 替换成新 shape 的array重新求值。
对比 PyTorch:
| 步骤 | PyTorch 2.3 (Eager) | PyTorch 2.3 (torch.compile) | MLX |
|---|---|---|---|
mx.matmul | 立即算,立即 alloc buffer | trace 期内不真算 | 不真算(lazy) |
| 改变 shape 重跑 | 无副作用 | 可能触发 re-compile | 不重 trace |
print | 隐式 .cpu().numpy() | 隐式 .cpu().numpy() | 隐式 eval() 落图 |
| 多后端切换 | .to('mps') / .to('cuda') | .to(...) | 不暴露 device API |
5. 真实工作负载:benchmarks 解读
mlx-examples/benchmarks/ 提供了几个标准对比表,但这些数字的测量对象必须先讲清楚,否则容易误读。
5.1 训练吞吐:LLaMA-7B 微调
mlx-examples/lora/ README 给出的官方数字(来源:ml-explore/mlx-examples 仓库 lora/README.md,截至 2026-06-17):
| 平台 | 设备 | tokens/sec | 备注 |
|---|---|---|---|
| MLX | M2 Ultra | ~475 | LLaMA-7B LoRA 微调 |
| MLX | M1 Max (32GB) | ~250 | 同上 |
README 未给出 PyTorch + MPS / CUDA 的对照数字,社区 issues 中有零星复现报告但条件不一(batch size、seq_len、量化配置差异大),不具备直接可比性,这里不引用。如需跨框架对比,建议自行在同一硬件上跑 lora/train.py 与等价 PyTorch 脚本。
测的是什么:纯 forward + backward 的 tokens/sec,不包含数据加载、tokenizer、checkpoint 落盘。
能推出什么:
- Apple silicon 的 M2 Ultra 在 7B 模型 LoRA 微调上对个人研究者是可用平台,不是生产训练平台。
- 同芯片系列内,统一内存容量是吞吐的主要约束(M2 Ultra 192GB vs M1 Max 32GB,差距接近 2×)。
不能推出什么:
- “MLX 比 A100 快”——README 没有这个断言,社区复现也显示 A100 在 7B 训练上吞吐高一个数量级以上。
- “Apple silicon 可以取代 CUDA 训练”——小模型 + 小 batch + 大显存的边缘场景下 yes,主流 LLM 训练仍必须 CUDA。
5.2 推理延迟:Whisper / Stable Diffusion
mlx-examples/stable_diffusion/ 和 mlx-examples/whisper/ 的 README 没有给出标准化的性能数字(只有使用说明和安装指南),因此本节不给具体 tokens/sec 或 s/步 的表格。社区 issues 中有用户自报数据,但硬件配置、模型版本、量化档位差异太大,不具备引用价值。
定性结论(基于 README 描述与社区反馈的共识):
- Stable Diffusion 在 M2 Max 及以上芯片上做本地预览(单张生成在 10 秒量级)可行;批量生产(>1000 张/小时)仍需 A100 / H100。
- Whisper 在 M 系列芯片上的推理速度足以支撑实时转录(小于音频时长),但 SLA < 1s 的服务场景仍要 GPU 服务器。
共性结论(适用于 MLX 给出的所有数字):
- MLX 的 benchmark 都是 Apple silicon vs 自身 或 同硬件跨框架,没有"MLX 跑得比 H100 快"这种断言。
- MLX 的性能优势在 fused kernel(把 LayerNorm + matmul + RoPE fuse 一次,减少往返 unified memory 的 RTT);不是单算子绝对速度。
6. 构建与上手:最少必要配置
6.1 Python 用户(90% 的情况)
# macOS Apple silicon
pip install mlx
# Linux + CUDA(实验性,需要从源码构建)
git clone https://github.com/ml-explore/mlx
cd mlx
pip install -e .注意:pip install mlx 默认是 macOS 预编译 wheel,里面包含 Metal backend、不包含 CUDA。Linux 上要 CUDA backend 必须源码 build。
6.2 源码构建(自定义 backend / 关掉 Metal)
cmake -B build \
-DMLX_BUILD_METAL=ON \
-DMLX_BUILD_CPU=ON \
-DMLX_BUILD_CUDA=OFF \
-DMLX_BUILD_TESTS=ON \
-DMLX_BUILD_BENCHMARKS=ON \
-DMLX_METAL_JIT=OFF
cmake --build build -j$(sysctl -n hw.ncpu)
# 跑测试
ctest --test-dir build --output-on-failure6.3 第一个会踩的坑
print在compile函数内不输出:compile(fn)trace 时 tracer 不会真算,print会被吞。调试时用mx.eval(y)显式落图。numpy互转走的是 implicit eval:mx.array(np_arr)是 O(1)(直接挂 buffer),np.array(mx_arr)强制 eval 整个图。大循环里写后者会爆显存。is_tracer=True的数组不能item():tracer 没有 buffer,强行item()报invalid argument: item can only be called on evaluated arrays。- CUDA backend 是实验性:
MLX_BUILD_CUDA=ON后编译能过,但mlx/distributed/的 ring 在 CUDA 上稳定度低于 Metal,社区 issue 较多。
7. 与其他框架的边界
MLX 不是 PyTorch 替代品,也不是 JAX 复制品。三个框架的取舍差异:
| 维度 | PyTorch 2.3 | JAX 0.4 | MLX |
|---|---|---|---|
| 默认执行 | Eager | Trace + jit | Lazy graph |
| 设备 API | .to(device) 显式 | jax.devices() 显式 | 不暴露 |
| 后端 | CPU / CUDA / MPS / XPU | CPU / GPU / TPU | CPU / Metal / CUDA |
| 分布式 | NCCL/Gloo | pjit + pmap | 自研 Ring over socket |
| 追踪 vs 捕获 | torch.compile 走 trace | jit 走 trace | 默认 lazy + compile() |
| 主要场景 | 通用研究 + 工业生产 | Google 内部 + TPU 工作流 | Apple silicon 研究 + 轻量推理 |
什么时候选 MLX:
- 在 Apple silicon(MacBook Pro / Mac Studio / M2/M3/M4 系列)上做 ML 实验,想避免
.to('mps')噪音。 - 个人研究者跑 LLaMA-7B / Stable Diffusion 这类中等模型,希望用一台 Mac 替代小型 GPU 服务器。
- 给现有 C++ 工程嵌入 ML 推理,MLX 的 C++ API 接近"加一个 Eigen 依赖"的心智。
什么时候不选 MLX:
- 需要 A100/H100 级别吞吐(必须 CUDA + PyTorch / vLLM / SGLang)。
- 需要成熟的 checkpoint 格式 + 模型 zoo(HuggingFace 生态以 PyTorch 权重为主,MLX 权重需
mlx-examples转换脚本)。 - 多机多 GPU 训练(MLX 分布式还在早期,ring over socket 性能不如 NCCL)。
8. 适用边界与采用顺序
8.1 推荐采用顺序
- 第一周:在 MacBook Pro M-series 上跑通
mlx-examples/llms/llama的 generate.py +mlx-examples/stable_diffusion的 1 张图 demo,确认 unified memory 路径工作。 - 第二周:把现有 PyTorch
nn.Module中纯 forward 的部分用mlx.nn.Module改写,对照 benchmark。如果 forward 里有大量 Python 控制流(if/loop over tensor),MLX 比 PyTorch 体感更自然。 - 第三周起:训练任务上 LoRA/QLoRA,跑
mlx-examples/lora;分布式先不上,等mlx.distributed在 CUDA backend 稳定后再评估。 - 生产部署:MLX 目前的生态位是"个人/小团队本地推理 + 实验性微调",不建议直接用于对外服务(缺 serving、缺监控、缺 model registry)。
8.2 已知边界
- 不支持 x86_64 Mac:
MLX_ENABLE_X64_MAC=OFF是默认(截至撰写时,选项位置见CMakeLists.txt顶层)。Intel Mac 用户要自己打开这个选项,但 Metal kernel 在 Intel iGPU 上没有可用测试矩阵。 - CUDA backend 仍是 experimental:
MLX_BUILD_CUDA=ON跑通编译容易,跑通大规模矩阵乘仍需调优。 - 生态较窄:模型 zoo、checkpoint 转换、serving、quantization 工具链都没 PyTorch 完整。HuggingFace 权重需要
mlx-examples提供的转换脚本(mlx-examples/llms/llama/convert.py等)。 compile函数有 trace cache 上限:高频调用的极小函数(如 attention 里的 single-token matmul)建议手动mx.compile,否则会反复求值。
8.3 一段话总结
MLX 做的事情是把 Apple silicon 的 unified memory 暴露成 array framework 的一等公民,让 CPU/GPU 的差别从 API 表面消失——写 a + b 不需要先 .to('mps'),跨 device 也不需要显式搬运。在 Apple silicon 上做研究、跑中小模型推理、做 C++ 嵌入,这三个场景下 MLX 在 2026 年这个时间点是同硬件里把"统一内存 + lazy graph + Metal JIT"三件事整合得最完整的选项,PyTorch MPS 后端在 device plumbing 与 fused kernel 覆盖度上都还有差距;其他场景(A100/H100 级吞吐、成熟 checkpoint 生态、多机多 GPU 训练),仍是 PyTorch + CUDA 的天下。
9. 自测题与进阶路径
9.1 自测题
下面 5 题用来检验读完本文后是否真的能上手 MLX。答案都在正文或仓库源码里,建议先自己查再对答案。
- lazy 落图:写
a = mx.array([1.0, 2.0]); b = mx.array([3.0, 4.0]); c = a + b,此刻c.array_desc_->status是哪个值?如果接着调print(c),status 会经历哪几次转换? - 统一内存:MLX 的
data()注释说"may do a copy if the underlying buffer is not accessible on the CPU"。既然是 unified memory,什么情况下会真的触发这次 copy?提示:看 backend 实现,不是所有 backend 都走 macOS unified memory。 - 动态图 vs trace:用
mx.compile(layer)包一个含if x.shape[0] > 1024分支的函数,第一次用 shape(512,)调用,第二次用 shape(2048,)调用。MLX 会重 trace 吗?PyTorchtorch.compile在同样场景下会怎样? - benchmark 误读:看到"MLX M2 Ultra 14k tokens/sec vs PyTorch+CUDA A100 120k tokens/sec",能不能推出"MLX 在 Apple silicon 上已经接近 A100"?为什么?提示:测的是什么、数字反映哪部分系统。
- CMake 选项:
MLX_BUILD_CUDA=OFF是默认,为什么?如果想在 Linux + CUDA 上跑 MLX,需要改哪些选项,又有哪些功能(如mlx.distributed)的稳定度会低于 Metal 后端?
9.2 进阶路径
读完本文后,按下面三条路径之一继续深入。
路径一:在 Apple silicon 上做研究/推理
- 跑通
mlx-examples/llms/llama的generate.py,确认 unified memory 路径工作。 - 把现有 PyTorch
nn.Module中纯 forward 的部分用mlx.nn.Module改写,对照 benchmark。 - 上 LoRA/QLoRA,跑
mlx-examples/lora;分布式先不上,等mlx.distributed在 CUDA backend 稳定后再评估。 - 进阶:读
mlx/backend/metal/kernels/下的 MSL 源码,理解 fused kernel 如何减少 unified memory 往返 RTT。
路径二:想理解框架设计差异
- 对照本文第 7 节的框架对比表,把 PyTorch、JAX、MLX 三者的执行模型(Eager / Trace / Lazy graph)写成自己的笔记。
- 读
mlx/array.h与mlx/compile.cpp,重点看Status枚举与compile的 trace 路径。 - 对比 PyTorch 2.x 的
torch.compile实现(torch/_dynamo/),看两者在"分支处理"上的差异。 - 进阶:读
mlx/distributed/ring/ring.cpp,对比 NCCL 的 Ring 算法实现,理解为什么 MLX 选择 socket/TCP 而不是 NCCL。
路径三:想给 MLX 贡献代码
- 先跑通源码构建(本文第 6.2 节),确认
ctest全绿。 - 从
mlx/primitives/里挑一个简单算子(如Add),读它的 CPU / Metal / CUDA 三套实现,理解 backend 注册机制。 - 在
mlx/backend/metal/kernels/里找一个 fused kernel,尝试改写它的 tile 大小,跑 benchmark 看变化。 - 进阶:挑一个
good first issue(仓库标签里筛),从修 Bug 开始熟悉贡献流程。
9.3 何时不必深入
- 你的硬件是 Intel Mac 或 Windows PC:MLX 的 Metal backend 只支持 Apple silicon,CUDA backend 仍是 experimental,Windows 没有官方支持。
- 你的场景是大规模生产训练:MLX 的分布式还在早期,ring over socket 性能不如 NCCL,应该用 PyTorch + CUDA。
- 你需要的是模型 serving 而不是研究:MLX 缺 serving、监控、model registry,应该看 vLLM / SGLang / TGI。
附录 A:关键文件索引
| 路径 | 作用 |
|---|---|
mlx/array.h | array 类定义,ArrayDesc 字段,状态机 |
mlx/array.cpp | eval() / reshape() / 拓扑遍历 |
mlx/compile.cpp | compile() 入口,trace 出 Compiled 函数 |
mlx/primitives/Add.h Add.cpp | 加法 Primitive,加 backend 调度 |
mlx/primitives/Convolution.* | 卷积 Primitive |
mlx/primitives/Embedding.* | Embedding 查表 |
mlx/backend/metal/kernels/ | MSL 源码,JIT / 预编译路径 |
mlx/backend/metal/primitives/ | 每个 Primitive 的 Metal eval 实现 |
mlx/backend/cuda/ | CUDA backend 同构实现 |
mlx/backend/accelerate/ | macOS Accelerate.framework BLAS 包装 |
mlx/distributed/ring/ | Ring allreduce over TCP/Unix socket |
mlx/io/load.mm | safetensors / GGUF 加载 |
python/src/array.cpp | pybind11 暴露的 array Python 类型 |
python/src/ops.cpp | Python mx.add / mx.matmul 等函数 |
python/src/nn/ | Module / Linear / Conv2d / MultiHeadAttention 等 |
python/src/optimizers/ | SGD / Adam / AdamW 等 |
CMakeLists.txt | 顶层构建配置,MLX_BUILD_* 选项 |
mlx/version.h | 版本号(major.minor.patch) |
附录 B:常用命令
# 安装
pip install mlx
# 源码构建(macOS)
git clone https://github.com/ml-explore/mlx
cd mlx && pip install -e .
# 跑测试
cmake -B build -DMLX_BUILD_TESTS=ON
cmake --build build -j
ctest --test-dir build --output-on-failure
# 跑 benchmark
cmake -B build -DMLX_BUILD_BENCHMARKS=ON
./build/bin/benchmarks
# 切换默认 device
mx.set_default_device(mx.gpu) # 显式 Metal
mx.set_default_device(mx.cpu)附录 C:参考链接
- 仓库主仓:https://github.com/ml-explore/mlx
- 文档站:https://ml-explore.github.io/mlx/build/html/index.html
- Swift API:https://github.com/ml-explore/mlx-swift
- C API:https://github.com/ml-explore/mlx-c
- 示例库:https://github.com/ml-explore/mlx-examples
- 论文 / 设计讨论:仓库内无单独论文,README 与 docs 站是主要设计来源