对于手头只有一台最低配 MacBook Air(M4,16G 统一内存,256G 硬盘)的朋友来说,能不能本地跑一个 9B 模型,还能不能用起来?答案是可以,但"用哪个引擎跑"这件事,速度差得不是一点半点。
这一篇拿手上这台最低配 Air,把三条路线——Ollama、oMLX、MoXing——全跑了一遍,速度和工具调用能力都测了,数据摆出来,分享给大家。
测试环境
MacBook Air M4
内存: 16G 统一内存架构
硬盘: 256G SSD
推理: Apple M4 GPU (Metal) + CPU 混合
三个引擎都是最新版:
| 引擎 | 定位 | 底层 | 装法 |
|---|---|---|---|
| Ollama 0.32.9 | 最流行的本地 LLM 管理器 | 自带 runner | brew install ollama |
| oMLX 0.5.7 | Apple Silicon 原生推理服务器 | mlx-lm(Apple 官方) | brew tap jundot/omlx && brew install omlx |
| MoXing 0.1.38 | llama.cpp 的 Python 封装 | llama.cpp(Metal) | pip install moxing |
Ollama 的 MLX 底层似乎是把权重拆成几百个散装 blob 存起来,跑的时候走 Ollama 自己的 runner,绕了一道。要说原生调 MLX,那还是 oMLX(直接基于 Apple 官方的 mlx-lm)。MoXing 则是另一条路——直接跑 llama.cpp,走的是 GGUF 这条更通用的路子。
模型从哪来
测三个引擎需要模型。两个 9B 选手:
- qwen3.5:9b(Qwen3.5 系列,工具调用 + 编码都能打)
- omnicoder-2-9b(社区 coding 模型,Q4_K_M 量化,GGUF 格式)
下载路径各不一样,这里有个坑值得单独说:
Ollama 的模型基本不能直接给 oMLX 用。就我实测,Ollama 把 MLX 权重拆成了逐个 tensor 的 blob(我 hexdump 看过,每个 blob 是 长度前缀 + JSON 元数据 + 裸 tensor),oMLX 要的是标准的 config.json + model.safetensors + tokenizer.json 目录结构,两者对不上。
oMLX 这边,好消息是它原生支持 ModelScope 下载(内置了 ms_downloader,走 modelscope SDK)。Qwen 是阿里自家的,ModelScope 也是阿里的,所以 Qwen 系列的 MLX 版本在 ModelScope 上最全,国内直连比 HuggingFace 快得多:
# oMLX 从 ModelScope 拉 Qwen3.5 的 MLX 版
snapshot_download('mlx-community/Qwen3.5-9B-MLX-4bit', local_dir='~/.omlx/models/...')
MoXing 更省事,它能直接读 Ollama 下载好的用户上传的 GGUF。Ollama 的 GGUF 模型本来就是整个文件存成一个 blob,MoXing 找到这个 blob 的路径就能直接跑,但是要注意只能运行omnicoder-9b这样的用户上传的GGUF,而不是Ollama的官方的GGUF:
from moxing.ollama import OllamaClient
path = OllamaClient().get_model_gguf_path('carstenuhlig/omnicoder-2-9b')
# /Users/fred/.ollama/models/blobs/sha256-550e8f... (5.74 GB)
速度对比
测速统一用 300 token 长文本生成、关闭思考、热启动取三次平均。这是最贴近"实际持续生成"的数字,比工具调用那种只吐几十个 token 的短回复更能反映真实吞吐。
9B 模型
| 引擎 | 模型 | 量化 | 生成速度 |
|---|---|---|---|
| Ollama | omnicoder-2-9b | Q4_K_M (GGUF) | 4.6 tok/s |
| MoXing | omnicoder-2-9b(同一份 GGUF) | Q4_K_M | 5.0 tok/s |
| Ollama | qwen3.5:9b-mlx | NVFP4 (safetensors) | 5.1 tok/s |
| oMLX | qwen3.5 9B | 4bit | 6.5 tok/s |
冷启动(含模型加载):
| 引擎 | 模型 | 冷启动耗时 |
|---|---|---|
| Ollama | omnicoder-2-9b | ~62s |
| MoXing | omnicoder-2-9b | ~38s |
| Ollama | qwen3.5:9b-mlx | ~66s |
| oMLX | qwen3.5 9B | ~44s |
Ollama vs MoXing:同模型多轮统计
上面 omnicoder 那一行(4.6 vs 5.0)是整体平均值。为了看清楚 MoXing 到底快在哪,把同一个 GGUF 按「输入长短 × 输出长短」切成四个场景,每个场景跑 3 次取平均:
| 场景 | Ollama (tok/s) | MoXing (tok/s) | 加速比 |
|---|---|---|---|
| 短输入-短输出(60 tok) | 4.65 ± 0.94 | 5.00 ± 0.70 | 1.08× |
| 短输入-长输出(300 tok) | 4.68 ± 0.12 | 4.61 ± 0.56 | 0.99× |
| 长输入-短输出(60 tok) | 4.29 ± 1.13 | 4.77 ± 1.25 | 1.11× |
| 长输入-长输出(300 tok) | 4.74 ± 0.15 | 5.51 ± 0.09 | 1.16× |
| 整体平均 | 4.59 | 4.97 | 1.08× |
规律大致是:长输入场景 MoXing 优势大(11-16%),短输入场景差距小(0-8%)。原因大概在于 MoXing 默认开了 Flash Attention 和 KV cache 4-bit 量化,这些优化的收益主要落在 prefill(处理输入)阶段,输入越长省得越多。短 prompt 时 prefill 占比小,两边 decode 速度本来就接近,差距就摊薄了。
0.8B 模型
| 引擎 | 模型 | 量化 | 生成速度 |
|---|---|---|---|
| Ollama | qwen3.5:0.8b-mlx-bf16 | BF16 | 25 tok/s |
| oMLX | qwen3.5 0.8B | 8bit | 34.4 tok/s |
工具调用能力对比
速度只是一方面,工具调用能不能打,才算得上本地模型能不能干活的更关键一环。测试沿用之前那套方案——参照 BFCL / ToolBench / τ-bench 的方法论,23 个用例 × 中英双语 = 46 个测试,覆盖简单调用、并行调用、工具选择、无关场景、多轮串行、边界情况八个维度。
| 引擎 | 模型 | 通过率 | EN | ZH |
|---|---|---|---|---|
| Ollama | qwen3.5:9b-mlx | 67.4% (31/46) | 78.3% | 56.5% |
| oMLX | qwen3.5 9B 4bit | 65.2% (30/46) | 82.6% | 47.8% |
| Ollama | omnicoder-2-9b | 63.0% (29/46) | 78.3% | 47.8% |
分维度得分:
| 维度 | qwen3.5:9b-mlx (Ollama) | qwen3.5 9B (oMLX) | omnicoder-2-9b (Ollama) |
|---|---|---|---|
| simple | 0.67 | 0.67 | 0.67 |
| parallel | 1.00 | 0.90 | 0.96 |
| tool_selection | 0.75 | 0.75 | 0.75 |
| irrelevant | 0.25 | 0.25 | 0.50 |
| multi_turn | 0.38 | 0.21 | 0.08 |
| tau_retail | 0.50 | 0.50 | 0.00 |
| edge | 0.67 | 0.67 | 0.83 |
之前那篇测过 11 个 1B 及以下的小模型,用的就是同一套 46 用例的脚本。把这次的三个 9B 成绩放进去一起排:
| 模型 | 参数量 | 通过率 | 来源 |
|---|---|---|---|
| lfm2.5-thinking | 1.2B | 76.1% | 之前 |
| smollm2:1.7b | 1.7B | 76.1% | 之前 |
| granite4:350m-h | 340M | 67.4% | 之前 |
| qwen3.5:9b-mlx | 9B | 67.4% | 本篇 (Ollama) |
| qwen3.5 9B | 9B | 65.2% | 本篇 (oMLX) |
| qwen3.5:0.8b | 0.8B | 63.0% | 之前 |
| omnicoder-2-9b | 9B | 63.0% | 本篇 (Ollama) |
| qwen3:0.6b | 0.75B | 56.5% | 之前 |
| granite4:350m | 350M | 54.3% | 之前 |
| llama3.2:1b | 1.2B | 34.8% | 之前 |
| functiongemma | 268M | 17.4% | 之前 |
| gemma3:1b | 1B | 0.0% | 之前 |
第一眼有点意外:9B 的通过率(63-67%)并没有碾压小模型,甚至被 lfm2.5-thinking 和 smollm2:1.7b 反超。但仔细看能看出门道:
9B 的 parallel(并行调用)这次拿了满分。 qwen3.5:9b 在"同时查几个城市天气、同时调多个工具"这类场景拿到 1.0,和 lfm2.5 打平,超过其他所有小模型。一次回复里规划多个工具调用,这是"复杂工具编排"的能力,比单次调用更接近真实 agent 干活的场景。
lfm2.5 的 76% 是拿速度换的。 它跑了 668 秒,是 granite4:350m-h(60 秒)的十倍还多——thinking 机制让它每一步都先"想"再"做"。真实场景里没人愿意为一个天气查询干等十几秒。
更重要的是,9B 是通用模型。 lfm2.5、smollm2 这些是在"工具调用"这个窄场景里表现好,让它们写代码、读长文档、做复杂推理,往往就直接掉链子。9B 相对是"什么都能干一点"——编码、长上下文、复杂推理、工具调用,都不算太拉胯。
所以"应急"的准确含义是:手头只有一台 16G 的 Air、没有网、又需要本地模型处理一点稍微复杂的活时,9B 是那个能顶上的——它不算顶尖,但不会像 1B 小模型那样在复杂任务面前直接宕机。
反过来,小模型也有它的位置。别看完上面的对比就觉得 9B 全面压制。在"简单工具调用"这个专门场景里,有些小模型不输 9B,甚至反超:
- granite4:350m-h(349MB)通过率 67.4% 和 9B 打平,但简单调用(0.83)和多轮串行(0.79)两项都超过了 qwen3.5 9B 的 0.67 和 0.38
- 关键是它只要 349MB、60 秒就跑完整个测试,常驻内存几乎零负担,随叫随到
所以更实用的组合不是二选一,而是各存一个:常驻一个 granite4:350m-h 这种轻量模型处理高频的简单调用,需要复杂任务时再临时拉 9B 出来。 一个管"快",一个管"全",不冲突。
几个有意思的发现
同一个 GGUF,换引擎平均快个 8%,长输入快更多。 MoXing 跑的就是 Ollama 下载的那份 omnicoder-2-9b GGUF,字节都没变。就这次多轮统计来看,短输入场景基本持平(0.99-1.08×),长输入场景能快 11-16%。原因大概在 runner 的默认参数——Ollama 没开 KV cache 量化、Flash Attention、连续批处理这些优化,而 MoXing 启动时默认就全开了(-ctk q4_0 -ctv q4_0 --flash-attn on --cont-batching),这些优化的收益集中在处理长输入的 prefill 阶段。
oMLX 的原生 MLX 比 Ollama 的 MLX 兼容层是要快一些。 9B 那组是 6.5 vs 5.1 tok/s,快 27%。这组相对干净——两边都是 4bit 级量化(NVFP4 vs MLX 4bit),可比。0.8B 那组 oMLX 快 38%,但那里 Ollama 用的是 BF16 全精度、oMLX 用 8bit,量化更低,所以 38% 里有一部分是量化差异的功劳,不能全算到引擎头上。
工具调用能力基本跟引擎无关,主要还是取决于模型本身。 qwen3.5 9B 在 Ollama 里 67.4%,换到 oMLX 里 65.2%,基本持平。这大概说明换引擎不会明显提升模型的工具调用水平——引擎主要决定快慢,模型才决定好坏。要提升工具调用,得换模型或换量化,不是换引擎。
omnicoder-2-9b 的多轮串行算是短板。 "先翻译再发邮件"这种链式任务,omnicoder-2-9b 只有 0.08 分,qwen3.5 9B 有 0.38。它定位是 coding 模型,工具调用不是强项,尤其多步依赖的场景基本掉链子。
中文工具调用普遍比英文差。 三个 9B 英文都在 78-83%,中文只有 47-57%,掉 30 个点以上。即使 Qwen3.5 9B 这种中英都训过的模型也一样,大概率说明这些模型的工具调用能力主要还是英文训练数据喂出来的。
16G 内存确实吃紧,oMLX 的 SSD caching 算是个解。 9B 模型 4bit 量化后 5.6GB,加上 KV cache 和其他进程,16G 内存已经不宽裕。oMLX 的 tiered KV cache + SSD caching(把冷 KV 块落到 SSD,热块留内存)在这台机器上是有意义的,这是 Ollama 没有的。不过它要占硬盘,硬盘紧张的话就得掂量一下了。
选型建议
结合速度和工具调用,三条路线各管一段:
要省心、生态全:Ollama。 一条命令拉模型,全平台支持,缺点是速度稍慢。适合第一次接触本地模型、或者不在乎多等几秒的场景。
要速度、跑 GGUF:MoXing。 直接读 Ollama 下载的 GGUF,同一份权重平均能快个 8%,长输入场景更明显,Metal 后端自动检测。缺点是得 pip 装、自己配,生态不如 Ollama。
oMLX 呢? 原生调 mlx-lm,9B 快 27%、冷启动快 1/3,还带 SSD caching。但说实话,我自己最后把它卸了——MLX 的加速在 9B 这个体量上感觉意义不大,速度差距没多少,还不如直接用 llama.cpp 这类,更省空间也省事。它那个 SSD 缓存听着不错,但我这 256G 硬盘本来就不太够用,就先不折腾了。要是你硬盘宽裕、又主要在 Apple Silicon 上跑 MLX 模型,它还是值得一试的。
模型本身的话,工具调用场景首选 qwen3.5 9B,它的 parallel 能拿满分、多轮串行也最强;omnicoder-2-9b 留给纯 coding 用,别指望它做多步工具编排。
几点说明
最后交代一下这次对比的局限性。
速度和引擎那部分,受网络和存储的限制,没能从 HuggingFace 把全部模型的 GGUF 和 MLX 版本都下载齐,导致量化等级对不齐——BF16 对 8bit、NVFP4 对 4bit,不完全是同一个东西在比。所以"谁快多少"这个数字只能当个粗略参考,别拿去当精确结论。
工具调用那部分是另一回事:三套引擎跑的是同一套 46 用例的测试脚本,跟之前小模型的成绩可以直接排在一起比。这部分结论是站得住的——9B 没在工具调用上碾压小模型,但它是通用模型,需要应急处理复杂任务时,它还是能顶上的那个。