上一期 Qwen3.8 27B 算得最准,但有两个毛病:慢(一年的账要 11 分钟),以及有一次明明算对了,回答却被截断,什么都没说出来。这一期不换题、不换模型,只研究设置:在一台 48GB 的 Mac 上,怎么让它更快,扛得住长上下文,而且不会自己停下来。
实验设置
- 机器:一台 48GB 内存的 Apple Silicon Mac(M5 Pro)。
- 推理:oMLX 0.7.2,内存保护上限保持我原来的 40 GB 不变。
- 测速:直接调用 oMLX 的接口,不经过 harness;流式输出、贪心解码、关掉思考,每次输出 640 token 以上。一个短提示,一个约 2.5 万 token 的长提示。
- 长上下文:把 oMLX 自己的源码拼成约 12 万 token,在中间藏一句暗号,让模型找出暗号并总结这是什么软件。
- 内存:每 5 秒记录一次 oMLX 进程占用的内存。
每个配置只测了一次,内存里每次只放一个模型。
两种 MTP,不是一回事
推测解码的原理很简单:一个很小的「草稿」先猜接下来几个 token,大模型一次把这几个一起检查,猜对的部分就等于白赚。Qwen3.8 有两种方式做这件事:
- 外挂草稿头:一个单独的小文件(约 0.2 GB),挂在主模型旁边。这是我原来用的。
- 模型自带的 MTP 层:草稿层就在模型文件里,oMLX 把这种叫 Lightning MTP,对应另一个开关。
我一开始看到 Lightning MTP 的开关是关的,以为加速一直没开。其实外挂草稿头一直在工作:那 25 到 30 tok/s 本来就是加速后的速度。而 Lightning MTP 在我的模型上根本开不了,因为很多社区做的 4-bit 版本会把自带的 MTP 层删掉。我的那个版本里一层都没有。下载前可以这样数一下:
python3 -c "import json; m=json.load(open('model.safetensors.index.json'))['weight_map']; print(sum('mtp' in k for k in m))"
输出 0 就是没有。我换了 oMLX 作者自己做的 Qwen3.8-27B-oQ4e-mtp(17 GB,保留了 29 个 MTP 张量),oMLX 一识别出来就自动打开了 Lightning MTP。草稿也猜得更准了:每一轮平均产出的 token 从 2.0 到 2.3 个提高到 2.85 到 3.08 个。
TurboQuant:省内存,不提速,而且有个坑
TurboQuant 把上下文缓存(KV cache)压缩成 4-bit。它的作用是省内存,让同样的内存装下更长的上下文,并不能让生成变快。
坑在这里:它和外挂草稿头不能同时开。我在旧模型上两个都打开以后,oMLX 并不是只关掉其中一个,而是这个模型的整套设置都不加载了,日志里只有一行:
vlm_mtp_enabled and turboquant_kv_enabled cannot both be True; choose one speculative path per model
我当时有备份,马上恢复了。教训是:改 oMLX 设置前先备份,改完去日志里确认没有 Failed to load settings。换成自带 MTP 层的新模型以后,Lightning MTP 和 TurboQuant 就可以同时开了。
结果
| 配置 | 短提示 | 2.5 万 token | 12 万 token | 内存峰值 |
|---|---|---|---|---|
| A:原来的(外挂草稿头) | 28.8–30.6 tok/s | 24.3 tok/s | ✓ | 未测 |
| B:新模型 + Lightning MTP | 32.9 tok/s | 30.0 tok/s | ✓ | 31 GB |
| C:新模型 + Lightning MTP + TurboQuant | 34.9 tok/s | 27.3 tok/s | ✓ | 28 GB |
B 在长上下文时最快,但我选了 C。我的 oMLX 大约在 33 到 34 GB 时开始拒绝请求,而 B 在 12 万 token 时已经到了 31 GB。harness 现在允许「13 万输入 + 3.2 万输出」,B 在这个上限附近很可能撞线。C 慢了 9%,换来 3 GB 的余量。和原来比,C 短提示快约 15%,长上下文快 12%。
TurboQuant 省下的内存比我想的少,原因是 Qwen3.8 用的是混合注意力:64 层里只有 16 层存完整的上下文缓存,缓存本来就不大。
真正的等待:读入
长代码任务真正耗时的不是生成,而是读入。Qwen 读入长上下文的速度大约是每秒 300 token,12 万 token 第一次要读 7 分钟左右,生成反而只要几十秒。好在 oMLX 会缓存读过的前文,同一个会话里只有第一次慢。Ornith 读入快 5 倍左右:9.2 万 token 只用了 58 秒。
另一个我担心的情况也测了:刚跑完一个 12 万 token 的请求,缓存还占着内存,紧接着换一批内容再发 13.5 万 token。这种情况我以前被拒绝过,这次顺利通过,暗号也找到了。
「自己停了」的真凶:客户端的上限
上一期 Qwen 算对了却说不出来,原因不在 oMLX:oMLX 早就允许 20 万 token 的上下文。卡住的是 harness 里的设置:上下文 4.9 万、每步输出 1.6 万。模型想得久一点,就会撞上这个输出上限。
规则很简单:每一层的上限都要对齐,oMLX 的上下文上限要大于等于 harness 的「上下文 + 输出」。这样在 oMLX 拒绝之前,harness 会先自动压缩对话。我现在的设置:
| oMLX 上下文 / 输出 | harness 上下文 / 输出 | |
|---|---|---|
| Qwen3.8 27B | 204,800 / 32,768 | 131,072 / 32,768 |
| Ornith 35B-A3B | 131,072 / 32,768 | 98,304 / 32,768 |
可以直接抄的设置
oMLX 里这个模型的设置(只列出改过的项):
"Qwen3.8-27B-oQ4e-mtp": {
"mtp_enabled": true,
"turboquant_kv_enabled": true,
"max_context_window": 204800,
"max_tokens": 32768
}
改完要在 oMLX 后台重新加载设置,再去日志确认生效。
局限
每个配置只测了一次,短提示的速度差异在误差范围内。测速是直接调 oMLX,没经过 harness;harness 每一轮还要多带几千到一万多 token 的系统提示和工具定义,实际用起来会更慢。我没测「13 万输入加 3.2 万输出同时用满」的极端情况,没测 TurboQuant 对答案质量的影响,也没试 oMLX 的另一种推测解码 DFlash。数据只来自一台机器。