把 Qwen3.8-27B 跑在 Mac 上不难,难的是知道该期待多少速度、以及哪些加速选项真的有用。这篇记录一次完整的本地部署:环境、配置、实测数据。
结论先放这里:DFlash2 投机解码在代码生成上值得开(16.4 → 30–45 tok/s),在中文散文上完全不值得开。而决定这件事的是任务类型,不是模型权重——这一点和很多人的直觉相反。
一、运行环境
| 项目 | 值 |
|---|---|
| 芯片 | Apple M5 Pro,20 核 GPU |
| 内存 | 64 GB 统一内存 |
| 推理服务 | oMLX 0.6.3rc3 (build 2475) |
| 主模型 | orcarouter/Qwen3.8-27B-Uncensored-MLX 4-bit,15.70 GB |
| 草稿模型 | z-lab/Qwen3.8-27B-DFlash2,3.85 GB |
| 对照模型 | mlx-community/Qwen3.8-27B-4bit,15.70 GB |
模型权重按 commit 钉死,避免上游更新导致数据不可比:
| |
内存占用很宽松:主模型 15.7 GB + 草稿模型 3.85 GB,加上 KV cache 也就 25 GB 左右,64 GB 机器毫无压力。所以没必要为了省内存去用 2-bit,4-bit 是速度和质量的平衡点。
上下文我固定在 32K 而不是模型原生的 262K。长上下文吃统一内存、拉长 prefill,日常编码和问答 32K 足够。
二、oMLX 的 Profile 机制
这是整套配置的基础。oMLX 的 Profile 是挂在基座模型上的一组命名参数,开启 expose_as_model 后会以 <模型ID>:<Profile名> 的形式出现在 /v1/models 里,客户端直接当成不同模型来选:
| |
好处是不用碰基座模型的 System Default 配置。同一份权重,按任务切参数,实验配置和稳定配置互不干扰——某个 Profile 配坏了,其他的照常工作。
管理走 admin API,是标准的增删改查:
| |
三、DFlash2 是什么,以及它什么时候有用
投机解码的原理:一个小的草稿模型一次猜 N 个 token,再由 27B 主模型一次性验证这批猜测。猜对的部分直接采纳,猜错的丢弃重来。
关键在于:主模型验证 N 个 token 的成本,远低于逐个生成 N 个 token。所以只要草稿猜得够准,就是净赚。
于是全部收益都压在一个指标上——draft acceptance(接受率)。这是实测数据:
| 任务类型 | 接受率 | 每轮产出 token | DFlash 是否划算 |
|---|---|---|---|
| 长 Python 模块 | 76.4% | 4.23 | 划算 |
| 开放式中文散文 | ~38% | 1.61 | 不划算 |
块大小设为 5,意味着每轮猜 5 个 token。代码任务每轮能落袋 4.23 个,草稿和验证的固定开销被摊薄;散文每轮只有 1.61 个,大部分算力打了水漂。
代码结构性强——缩进、括号、import 语句、常见的函数签名,草稿模型很容易猜中;开放式中文写作发散,下一个词的可能性太多。
四、一个反直觉的发现
我用的是社区 abliterate(去拒答)版本的权重,而草稿模型是针对上游官方权重训练的。按常理,权重分布不一致会拉低接受率。
实测把这个假设推翻了:
| 目标模型 | 散文接受率 | 代码接受率 |
|---|---|---|
| Uncensored(abliterated) | 37.8% | 76.4% |
| 官方 4-bit(上游权重) | ~38% | 76.3% |
两个模型的接受率几乎完全一样。 差异全部来自任务类型,和权重是否被改造过无关。
原因其实很直白:abliteration 改的是"要不要拒绝回答",不是"怎么写 Python"。草稿模型需要预测的是后者,而那部分能力压根没被动过。
所以那句流传的说法——“改造过的权重配不上官方 drafter”——至少在代码任务上不成立。如果你也在跑社区微调版本,值得自己测一次再下结论。
五、实测数据
工作负载:让模型写一个线程安全的泛型 LRU 缓存,带 8 个以上 pytest 测试,输出 1400 token。
代码任务:DFlash 大幅领先
四组配对测试(同一时刻跑两个配置,交替先后顺序):
| 组 | 普通解码 | DFlash2 | 提升 |
|---|---|---|---|
| 1 | 16.37 | 30.58 | +86.8% |
| 2 | 15.84 | 34.37 | +117.0% |
| 3 | 16.89 | 41.45 | +145.5% |
| 4 | 16.63 | 45.15 | +171.5% |
配对差均值 +21.46 tok/s,四组方向一致。运行时快照确认 DFlash 真实生效(fallback_ar: false),不是静默回退到普通引擎。
中文散文:没有收益
同样的配对方法,五组的差值是 −3.81、−1.54、−0.74、+0.07、−3.57 —— 符号都不一致。这不是"提升很小",而是没有稳定效应。散文就该走普通解码。
峰值不等于稳态
这条最容易踩坑。连续跑五次同样的代码任务,不给机器喘息:
| 第几次 | 1 | 2 | 3 | 4 | 5 | 衰减 |
|---|---|---|---|---|---|---|
| 普通解码 | 14.33 | 13.96 | 13.38 | 13.40 | 13.52 | −6% |
| DFlash2 | 40.34 | 19.41 | 16.08 | 18.11 | 21.38 | −47% |
DFlash 衰减得比普通解码猛烈得多,而且这是 DFlash 自身的性质——不论它先跑还是后跑,结果都一样。合理的解释是:它跑得快,单位时间做的计算就多,机器进入降速状态也更快。
输出长度全程稳定在 1400 token,所以这不是长度差异造成的假象。约两分钟空闲后速度会恢复。
实践含义:交互式使用(问一句、看一会儿、再问一句)基本感受不到;批量循环会明显掉速。规划长时间编码会话时,按 16–21 tok/s 估算,别按 40。
顺带一提,如果你要自己测速度,别用连续跑 N 次取平均——那只是在衰减曲线上采样。正确做法是两个配置紧邻着跑成一对、交替先后顺序、每对之间冷却两分钟,然后比较配对差值。
六、生产配置
最终三个 Profile,客户端只靠 model ID 选行为:
| Model ID | 用途 | 配置要点 |
|---|---|---|
:chat | 对话、写作、翻译 | 关思考,关 DFlash,temp 0.7 |
:code | 代码、JSON、结构化输出 | 关思考,开 DFlash,temp 0.2 |
:reason | 多步推理、复杂分析 | 开思考,8K 输出预算 |
:code 的完整配置
| |
几个要点:
dflash_block_size: 5—— 量化模型官方建议不超过 5。dflash_draft_model必须是绝对路径。enable_thinking要同时写在settings和chat_template_kwargs里,再用forced_ct_kwargs强制生效,否则客户端可能覆盖掉。- 其余加速项全部显式设为
false。 这样任何速度变化都能归因到 DFlash 一项上,而不是几个开关混在一起说不清。
:reason 要当心
思考模式会把绝大部分预算花在隐藏推理上。实测一次约 800 字的中文写作请求:生成 5,647 token,其中 5,068 token(89.7%)是思考内容,耗时 413 秒。
所以 :reason 只留给真正需要多步推理的任务,日常问答走 :chat,否则等待时间会非常难受。
另外这个 Profile 目前没开 DFlash——不是测出来不行,而是压根还没测过。推理文本的可预测性介于代码和散文之间,接受率是多少不好凭空判断,等实测。
MTP 用不了
oMLX 有原生 MTP(多 token 预测)支持,但这个模型用不了,运行时给的原因很明确:
| |
config 里声明了 MTP 层,但权重文件里既没有 mtp.* 张量也没有原生 nextn 层——转换时丢了。官方 4-bit 版本同样如此。
七、客户端接入
任何 OpenAI 兼容客户端:
| |
冷启动需要载入 15.7 GB 权重,首次请求会等几秒;oMLX 默认 15 分钟空闲后卸载模型。如果介意这个延迟,可以在 UI 里把模型 pin 住常驻——代价是 15.7 GB 内存一直被占着,看你机器上还跑什么。
八、小结
- 4-bit 是 64 GB Mac 的合理选择,装得下更高精度,但没必要。
- 投机解码不是无条件加速,是一场关于"输出可预测性"的赌局。 代码赌赢(76% 接受率,快 2–2.7 倍),开放式散文赌输(38%,没收益)。
- 接受率由任务类型决定,不由权重是否被改造决定。 社区 abliterate 版本一样能吃到 DFlash 的红利。
- 投机解码不改变输出内容——每个 token 都经过主模型验证,所以这份速度不以质量为代价。
- 区分峰值和稳态。 40 tok/s 是休息充分时的数字,持续负载下按 16–21 tok/s 规划。