结论先给:M4 16GB 跑 Qwen3-8B 的 4bit 权重,MLX 短上下文解码 20.3 tok/s,Ollama 17.4 tok/s,快一成七;换成 3.3K token 的长文档,MLX 预填充反而比 Ollama 慢两成,把步长调到 8192 才追平。整套环境装完约十五分钟,四分钟在下载 4.61GB 权重。
下面每个数字都来自同一台机器(Apple M4、16GB 统一内存、macOS),同一份 Qwen3-8B 4bit 权重,同一条中文问题,各跑三次取中位数。不跨机器搬结论。
一、装环境:先隔离,别塞系统 Python
mlx-lm 会连带装 mlx、mlx-metal、transformers。直接 pip 进系统解释器,以后别的项目要 transformers 4.x 就降不下来。两条命令隔出干净环境。
python3 -m venv .venv
.venv/bin/pip install mlx-lm
实测 44 秒装完 35 个包,落下来的核心版本是 mlx 0.32.4、mlx-metal 0.32.4、mlx-lm 0.32.2,transformers 被拉到 5.22.2。装完必须确认 Metal 后端真的挂上了。
.venv/bin/python -c "import mlx.core as mx; print(mx.default_device())"
输出 Device(gpu, 0) 才算吃到 GPU。如果退回 Device(cpu, 0),说明 mlx-metal 没装上,后面所有速度数字都会差一个量级,别急着测性能。
二、下权重:MLX 只认 safetensors 目录
选 mlx-community 的 4bit 版本,权重是单个 4.61GB 的 safetensors。这里有个省钱的办法:配置类小文件用 hf 命令拉,大的权重文件用 curl 直链。
hf download mlx-community/Qwen3-8B-4bit --local-dir models/qwen3-8b-4bit \
--include "*.json" --include "*.txt"
curl -L -C - -o models/qwen3-8b-4bit/model.safetensors \
https://hf-mirror.com/mlx-community/Qwen3-8B-4bit/resolve/main/model.safetensors
原因在测速结果里:hf 命令走 xet 通道,我这边只有 2.7MB/s 上下,四分钟才到 655MB;换 curl 直链稳定在 17MB/s,四分半拉完。同一镜像不同时段速度能差好几倍,别把某次测速当成该镜像的定论,上一轮我测同一个站还拿到过 9.4MB/s 的数,两轮结论方向都不一样,那次的完整测速过程可以对照看。
下完先对哈希,再跑模型。仓库的 LFS 文件 sha256 与响应头里的 x-linked-etag 是同一串,本地算出来必须一模一样。
shasum -a 256 models/qwen3-8b-4bit/model.safetensors
我这轮拿到 f2d29621aab30033…,与镜像端一致。哈希对不上就是下载被截断,重下文件,不要去改 config.json 里的张量名。
三、起服务:一条命令给 OpenAI 兼容接口
mlx-lm 自带 HTTP 服务,不用另装 FastAPI 或 vLLM。
.venv/bin/mlx_lm.server --model models/qwen3-8b-4bit --port 8092 \
--host 127.0.0.1 --chat-template-args '{"enable_thinking": false}'
日志里出现 Starting httpd at 127.0.0.1 on port 8092 就算起来了,同时会带一条安全警告:这个服务只做了最基本的校验。--host 0.0.0.0 能让局域网内别的机器访问,但别拿它直接对公网。
客户端里填的模型名有讲究。GET /v1/models 返回的 id 是本地绝对路径,不是仓库名。
curl -s http://127.0.0.1:8092/v1/models
之后每次请求,model 字段都要原样抄这个路径。填 qwen3-8b 这类短名,服务端会把它当成 Hugging Face 仓库去查,日志里能看到对 huggingface.co 的一次 401,最后给客户端一个 404。
curl -s http://127.0.0.1:8092/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"上一步返回的 id","messages":[{"role":"user","content":"你好"}],"max_tokens":64}'
四、同机三方对比:MLX、Ollama、llama.cpp
三个引擎,一份 Qwen3-8B 的 4bit 权重,同一条约 40 字的中文问题,max_tokens 给 256,温度 0,各跑三次取中位数。llama-server 用的是 Ollama 本地那份 blob,权重逐字节相同,所以它和 Ollama 的数字可以直接判读。
| 引擎 | 权重格式 | 解码 tok/s | 内存 | 起服务的方式 |
|---|---|---|---|---|
| MLX(mlx-lm 0.32.2) | 4bit safetensors 4.61GB | 20.3 | 峰值 4.79GB | mlx_lm.server |
| Ollama 0.40.14 | q4_K_M 5.23GB | 17.4 | 常驻 5.59GB | ollama serve |
| llama.cpp(brew 装) | 同一份 q4_K_M | 17.9 | RSS 5.46GB | llama-server |
三个数放在一起能看出两件事。第一,同引擎同权重就是同一个速度:Ollama 17.4、llama-server 17.9,差的那 0.5 在误差里,因为它俩底层是同一套 llama.cpp。第二,MLX 快的一成七,量级不大,指望换引擎提速十倍会失望。
内存那 0.8GB 的差距也不能全算引擎效率。Ollama 的 q4_K_M 权重本身就 5.23GB,MLX 4bit 只有 4.61GB;再加上 llama.cpp 系按 num_ctx 预分配 KV,4096 上下文先扣 0.59GB(KV 每 1K 约 144MB 的推算过程写在这篇里),MLX 按实际长度增长,这次只生成 223 个 token。两笔加起来正好 0.8GB。换引擎省下的内存,本质是换量化格式省下的内存。
长文档是另一个故事。输入 3.3K token,让模型用一句话概括:
| 场景 | MLX 默认步长 2048 | MLX 步长 8192 | Ollama |
|---|---|---|---|
| 3.3K 输入预填充 | 145.3 tok/s | 194.3 tok/s | 186.5 tok/s |
| 峰值内存 | 5.63GB | 5.73GB | 6.19GB(ctx 8192 常驻) |
| 该段解码 | 14.8 tok/s | 15.5 tok/s | 16.8 tok/s |
结论与短上下文相反:长输入时 MLX 默认配置比 Ollama 慢两成,把预填充步长调大才反超 4%,代价是多吃 0.1GB 内存。所以做长文档摘要这类任务,--prefill-step-size 这个参数必须显式给。
.venv/bin/mlx_lm.generate --model models/qwen3-8b-4bit --prompt - \
--max-tokens 32 --prefill-step-size 8192 \
--chat-template-config '{"enable_thinking": false}' < long.txt
注意两个子命令的参数名不一样:mlx_lm.server 认 --chat-template-args,mlx_lm.generate 认 --chat-template-config。抄错会直接报 unrecognized arguments,而且报错信息只列出一长串合法参数,很不显眼。
五、三个会白等一整晚的坑
坑一,思考没关,正文是空的。Qwen3 系列默认先思考。Ollama 的 /api/generate 不带 think:false 时,256 个 token 全花在思考上,返回体里 response 是 0 字符、thinking 有 412 字,HTTP 照样 200,done 也是 true。MLX 的症状一样,content 空、reasoning 435 字、finish_reason 是 length。两边字段名还不同,客户端如果按 OpenAI 习惯去读 reasoning_content,两个引擎都读不到,只能看到空字符串。
坑二,model 字段乱填,404 来得很莫名。我一开始随手填了 x,客户端拿到 404 但服务端没报错,直到翻日志才看到它去 huggingface.co 查了 x 这个仓库,401 之后才 404。判断方法很简单:/v1/models 返回什么,请求里就填什么,一个字都别改。
坑三,拿 GGUF 目录喂 MLX。直接把 llama.cpp 转出来的 GGUF 放进目录,报的是 FileNotFoundError,缺的是 config.json。MLX 从 config.json 读网络结构,GGUF 里那套元数据它不认。反过来 Ollama 与 llama-server 也吃不进 MLX 的 4bit safetensors。要把 safetensors 转成 GGUF,流程见这个六步实操。两套格式想并存只能各存一份,磁盘要按两倍留。
六、并发:默认就在批处理
单请求生成 128 个 token 用了 7.51 秒,折算 17.05 tok/s。四个不同问题同时打进去,四个请求都在 8.49 秒返回,聚合 60.27 tok/s,是单请求的三点五倍。mlx_lm.server 默认就开批处理解码,不需要配 --decode-concurrency,这是它相对 Ollama 真实占优的地方。
对照数据来自同机四并发的 Ollama 实测,聚合 46.7 tok/s。MLX 这边高约三成。要做批量生成(比如一次给几百条商品文案改写),这个差距比单条解码的 17% 更值得在意。
七、怎么选:按场景定,别按跑分
要模型管理、多客户端、开着不管,选 Ollama。它的模型市场、ollama pull、keep_alive 这套东西省心,代价是内存按上下文预分配。
内存紧、想跑 Apple 原生、后续要 LoRA 微调,选 MLX。mlx-lm 自带 mlx_lm.lora,训练和推理用同一套权重格式,这条链路 Ollama 给不了。
要压满并发、能自己写调度,用 llama-server,n_slots 与连续批处理都能调。8GB 内存的机器别硬上 8B,换 4B 的 4bit 版本,权重约 2.3GB,留够 KV 和系统的余量。
常见问题
MLX 能直接用 Ollama 下载好的模型吗
不能。Ollama 存的是 GGUF blob,MLX 需要 config.json 加 safetensors 的目录结构,两者元数据不互通。同一模型要在两边都跑,得各下一份,磁盘按两倍算。
mlx_lm.server 返回的内容为什么是空的
九成是思考模式没关。启动时加 --chat-template-args '{"enable_thinking": false}',或者客户端在消息里带关闭标记。判断依据是 finish_reason 为 length、completion_tokens 吃满上限,同时 content 为空、reasoning 有内容。
MLX 比 Ollama 快多少,值得换吗
短上下文快约一成七,长文档预填充默认还慢两成,调大 prefill 步长才追平。做批量并发生成 MLX 高约三成。只看单条对话,换引擎的收益小于把模型从 8B 换成 4B。
延伸阅读
- 法官裁定苹果可获取 OpenAI 招聘材料:AI 人才争夺战首例商业秘密判决4 周前
- 算力不够:苹果确认初代HomePod mini无缘全新Siri AI4 周前
- whisper.cpp 中文转写实测:五档模型怎么选1 天前
- 阶跃星辰 600B MoE 旗舰 Step 5 Preview 上线 22 小时 GPU Kernel 优化到 508 TFLOPS2 月前
继续读
同一批更新里这几篇值得一看:
- safetensors 转 GGUF:六步跑通与三处静默失败2026-11-12
- whisper.cpp 中文转写实测:五档模型怎么选2026-11-11
- hf-mirror 下载模型:实测 9.4MiB/s 魔搭快 2.8 倍2026-11-10
想快速查定义?对应的词条在这里:
- Llama词条 · AI 自学笔记
