MLX 跑 8B 模型实测:比 Ollama 快 17%,长文反而慢

结论先给: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。