Qwen3.8-27B on M2 Max:20 tok/s 还能不能再快?

oMLX 0.7.3.dev2 · Apple M2 Max(38 核 GPU / 64 GB / 实测带宽 348 GB/s)· 全部数据来自本机实测
结论:你的 20 tok/s 已经撞上内存带宽墙,剩下的空间不在「调参」里。

本机实测 M2 Max 可持续内存带宽 348 GB/s,模型权重 15.13 GB,由此算出的 decode 理论上限是 23.0 tok/s。绕过 oMLX 直接用 MLX 跑同一个模型,实测 22.2 tok/s —— 相当于已经吃掉了 96% 的可用带宽。oMLX 报的 17.5–20 tok/s 只是叠加了调度与采样开销。

所以真正的选择只有三个:减小权重体积、一次读盘服务多个请求、一次读盘吐多个 token。 第三条的 MTP 你已经验证过基本无效,本文给出原因和剩下两条的实测数字。

1. 先把天花板算清楚

LLM 解码的本质非常朴素:每生成一个 token,就要把全部权重从内存读一遍。于是

decode 上限 (tok/s) = 内存带宽 (GB/s) ÷ 权重体积 (GB)

我用 MLX 在 GPU 上做了大矩阵 matvec 微基准,得到这台机器的真实带宽(不是标称值):

访存模式实测带宽说明
bf16 大矩阵 matvec348.8 GB/s接近峰值,代表机器上限
大数组归约(STREAM 式)337.8 GB/s纯读带宽,互相印证
4bit 量化 matvec(248320×5120)325.9 GB/sdecode 真实 kernel
4bit 量化 matvec(5120×5120)39.0 GB/s小矩阵严重受 launch 开销限制

注:M2 Max 标称 400 GB/s,实测可持续约 348 GB/s。上面最后一行也解释了为什么「层数多、每层小」的模型 在 MLX 上跑不满带宽——小矩阵根本喂不饱 38 个 GPU 核。

15.13 GB
Qwen3.8-27B-4bit 实际权重
348 GB/s
M2 Max 实测可持续带宽
23.0
理论上限 tok/s
22.2
MLX 裸跑实测 tok/s(96% 效率)

2. 位宽是唯一能真正突破的杠杆(实测)

我把 4bit 模型逐层重量化成 3bit / 2bit,同模型同数据做对比。速度用 MLX 裸跑测, 质量用同一段中英混合文本的困惑度(PPL,越低越好)衡量。

位宽权重decode tok/s提速 有效带宽PPL质量代价
4bit(官方现用)15.13 GB22.001.00× 333 GB/s (96%)4.813基准
3bit(本次重量化)11.93 GB25.72 1.17×307 GB/s (88%)5.715 PPL +19%
2bit(本次重量化)8.73 GB31.73 1.44×277 GB/s (80%)16.871 PPL +250%(崩坏)
decode 速度(tok/s)
4bit · 22.0
3bit · 25.7
2bit · 31.7(质量崩)
怎么读这张表

3. 你的场景是「推荐」,批处理才是最大杠杆

降低位宽提升的是单请求延迟。但如果你的推荐任务是一次给多个候选项打分/批量生成, 那么把请求并发起来,一次读盘服务多个序列,总吞吐可以数倍提升——而且完全不损失质量。

并发数总生成吞吐相对 1×说明
121.5 tok/s1.00×基线
234.3 tok/s1.59×
433.6 tok/s1.56×
1650.9 tok/s2.37×本次测得的最高吞吐

上表用 mlx-lm 的静态 batch 测得,噪声偏大(8× 处有回退)。oMLX 自己用的是 continuous batching + mlx-lm BatchGenerator,官方在 M3 Ultra 上给出 8× 并发 3.36–4.14× 的数字, 实际效果应好于这里的静态 batch。单请求延迟不会变快,但跑完一批的总时间会大幅缩短。

4. 为什么草稿模型(MTP)在你这里没用

你观察到「推测 4 个 token 和不用草稿一样快」,这不是你的错觉,源码层面有三条硬约束:

  1. 你的 checkpoint 里根本没有 MTP 权重。服务日志明确写着 config declares mtp heads but checkpoint ships no mtp.* weights, 所以 oMLX 原生的 Lightning MTP(mtp_enabled)永远无法启用, 只能走外部草稿模型的 vlm_mtp_enabled 路径。
  2. 草稿深度被自动夹回 3。外部草稿 Qwen3.8-27B-MTP-4bit 的 block_size=3,而 mlx-vlm 只有在最近若干轮接受率 ≥65% 时才允许放大, 否则一律夹回 3 —— 每轮实际只多产出约 2.31 个 token。你手动设 4 / 5 基本不生效。
  3. 验证阶段把省下的带宽又烧回去了。草稿验证存在逐 token 回退路径,会让权重在一轮内被重复读取; 且 qwen3_5_mtp 每轮都会触发一次全量 synchronize + clear_cache。 反应到数字上:一轮耗时约为单步的 2.12 倍,却只多吐 2.31 个 token。

用你自己的日志做同 prompt 严格对比:不开 MTP 19.0 tok/s,开了 20.7 tok/s, 只有 +9%。而且它在并发时会整条请求回退到普通 BatchGenerator。

5. oMLX 里那批「全部关闭」的开关,逐个诊断

~/.omlx/model_settings.json 里针对该模型有一批开关,当前全是 false。 我把它们都翻了一遍源码,结论是大部分只作用于 prefill,对你的 decode 一点用没有:

开关作用阶段能提速 decode?判定依据
turboquant_kv_enabled(KV 4bit)decode 几乎不能见下方上下文扫描:4k 上下文收益约 2%
vlm_mtp_enableddecode+9% 与 turboquant_kv 互斥,二选一
qwen35_ane_prefill_enabledprefill不能 准入要求输入恰好 2048 行,decode 是 1 行,永不命中
qwen35_oq_a8_enabledprefill不能 需要 M5+ 原生 INT8 张量单元,M2 Max 被 API 直接拒绝
specprefill_enabledprefill不能 需另配草稿模型;且会丢 token,掉质量
dflash_enableddecode无法启用 模型目录里没有 dflash 草稿权重
qwen4_gdn_decode_wide_projdecode不适用 只对 Qwen4-Exp 生效,你是 qwen3_5
moe_expert_offload_enabled—不适用 这是 dense 模型,没有专家层

上下文长度几乎不影响 decode(这就是 KV 量化没用的原因)

prompt 长度KV 体积decode tok/sms/token
322.1 MB22.2345.0
51233.6 MB22.1845.1
2048134.2 MB21.4646.6
4096268.4 MB21.5346.4

上下文从 32 涨到 4096,decode 只掉了 3%。原因是这个模型是混合架构: 64 层里 48 层是 GDN 线性注意力(状态大小固定,不随上下文增长),只有 16 层是 full attention。 每 token 的 KV 仅 64 KiB,4k 上下文也才 268 MB,相对 15.13 GB 的权重读取完全可以忽略。 你实际平均 prompt 约 1.7k token,开 KV 量化的收益不到 2%,还要承担有损压缩的风险——不建议。

6. 环境排查(都不是瓶颈)

检查项结果
Ollama 后台是否抢带宽否,/api/ps 返回空,未加载任何模型
内存压力系统空闲 93%,无 swap 颠簸
SSD KV 缓存已启用,4.17 GB,只影响多轮前缀复用
oMLX 相对裸跑的开销22.2 → 17.5–20 tok/s,约 9–20%(调度 + 采样 + 流式)

7. 可执行清单(按性价比排序)

  1. 把推荐任务改成批量并发 零质量损失 吞吐 2–3×
    一次灌入多个候选项而不是逐个请求。这是你「高效推荐」场景下唯一不牺牲质量的大幅提速手段。
  2. 换 3bit 权重 +17% 速度 PPL +19%
    优先找社区用重要性矩阵做的正式 3bit 版本(质量优于本次二次量化)。 我已在本地生成了一版可直接加载的 3bit:
    ~/.cache/huggingface/hub/local--Qwen3.8-27B-3bit-gs64(11.93 GB,25.7 tok/s)
  3. 想要更大突破就上 2bit / 三进制 +44% 速度或更多
    务必用社区正式版而非二次量化。参考 prism-ml/Ternary-Bonsai-2-27B-mlx-2bit (8.85 GB,官方称保留 98.2% 基准分),注意它可能需要配套 kernel 才能跑满。
  4. 可以顺手关掉的开销:若不需要推理链,把 enable_thinking 关掉, 能减少大量无谓的生成 token(不改变 tok/s,但显著缩短端到端等待)。
  5. 不建议做:开 KV 量化(<2% 收益且有损)、继续调 MTP 深度(被代码夹死)、 指望 ANE / OptiQ A8(M2 Max 用不了且只管 prefill)。

真实预期

把上面 1 + 2 组合起来:单请求 25.7 tok/s,批量吞吐 60+ tok/s。 想要更高,只能换更小的模型或更高带宽的机器——M2 Max 的 348 GB/s 是硬约束, 任何软件优化都不可能让它凭空变出带宽。