【本地部署】64GB Mac 跑 Qwen3.8-27B:oMLX Profile 配置与 DFlash2 投机解码实测

M5 Pro / 64GB 上用 oMLX 部署 Qwen3.8-27B-Uncensored,DFlash2 投机解码在代码任务上把 16.4 tok/s 拉到 30–45 tok/s。附完整 Profile 配置、acceptance 实测数据,以及一个反直觉的结论:draft 接受率取决于任务类型,而不是权重是否被 abliterate 过。

把 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 钉死,避免上游更新导致数据不可比:

1
2
drafter : 50307d4c4cde6860d4eee73e2547cd786fe8e8a4
official: 3e6447f082e89cc7f0bc6e5441afd38dfce760ff

内存占用很宽松:主模型 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 里,客户端直接当成不同模型来选:

1
2
3
Qwen3.8-27B-Uncensored-MLX:chat
Qwen3.8-27B-Uncensored-MLX:code
Qwen3.8-27B-Uncensored-MLX:reason

好处是不用碰基座模型的 System Default 配置。同一份权重,按任务切参数,实验配置和稳定配置互不干扰——某个 Profile 配坏了,其他的照常工作。

管理走 admin API,是标准的增删改查:

1
2
3
4
5
POST   /admin/api/login
GET    /admin/api/models/{model_id}/profiles
POST   /admin/api/models/{model_id}/profiles          # 新建
PUT    /admin/api/models/{model_id}/profiles/{name}   # 更新
DELETE /admin/api/models/{model_id}/profiles/{name}

三、DFlash2 是什么,以及它什么时候有用

投机解码的原理:一个小的草稿模型一次猜 N 个 token,再由 27B 主模型一次性验证这批猜测。猜对的部分直接采纳,猜错的丢弃重来。

关键在于:主模型验证 N 个 token 的成本,远低于逐个生成 N 个 token。所以只要草稿猜得够准,就是净赚。

于是全部收益都压在一个指标上——draft acceptance(接受率)。这是实测数据:

任务类型接受率每轮产出 tokenDFlash 是否划算
长 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%

两个模型的接受率几乎完全一样。 差异全部来自任务类型,和权重是否被改造过无关。

draft 接受率对比:同一任务下两种权重几乎相同,跨任务差异巨大

原因其实很直白:abliteration 改的是"要不要拒绝回答",不是"怎么写 Python"。草稿模型需要预测的是后者,而那部分能力压根没被动过。

所以那句流传的说法——“改造过的权重配不上官方 drafter”——至少在代码任务上不成立。如果你也在跑社区微调版本,值得自己测一次再下结论。

五、实测数据

工作负载:让模型写一个线程安全的泛型 LRU 缓存,带 8 个以上 pytest 测试,输出 1400 token。

代码任务:DFlash 大幅领先

四组配对测试(同一时刻跑两个配置,交替先后顺序):

普通解码DFlash2提升
116.3730.58+86.8%
215.8434.37+117.0%
316.8941.45+145.5%
416.6345.15+171.5%

代码任务四组配对测试:普通解码稳定在 16 tok/s,DFlash2 从 30.6 升至 45.2 tok/s

配对差均值 +21.46 tok/s,四组方向一致。运行时快照确认 DFlash 真实生效(fallback_ar: false),不是静默回退到普通引擎。

中文散文:没有收益

同样的配对方法,五组的差值是 −3.81、−1.54、−0.74、+0.07、−3.57 —— 符号都不一致。这不是"提升很小",而是没有稳定效应。散文就该走普通解码。

峰值不等于稳态

这条最容易踩坑。连续跑五次同样的代码任务,不给机器喘息:

第几次12345衰减
普通解码14.3313.9613.3813.4013.52−6%
DFlash240.3419.4116.0818.1121.38−47%

连续五次运行的衰减曲线:DFlash2 从 40.3 跌到 21.4,普通解码几乎持平

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 的完整配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
  "name": "code",
  "api_name": "code",
  "expose_as_model": true,
  "settings": {
    "max_context_window": 32768,
    "max_tokens": 4096,
    "temperature": 0.2,
    "top_p": 0.9,
    "top_k": 20,
    "enable_thinking": false,
    "chat_template_kwargs": { "enable_thinking": false },
    "forced_ct_kwargs": ["enable_thinking"],

    "dflash_enabled": true,
    "dflash_draft_model": "/绝对路径/Qwen3.8-27B-DFlash2",
    "dflash_block_size": 5,
    "dflash_draft_quant_enabled": true,
    "dflash_draft_quant_weight_bits": 4,
    "dflash_draft_quant_activation_bits": 16,
    "dflash_draft_quant_group_size": 64,
    "dflash_max_ctx": 8192,
    "dflash_in_memory_cache": true,
    "dflash_verify_mode": "dflash",

    "turboquant_kv_enabled": false,
    "qwen35_ane_prefill_enabled": false,
    "mtp_enabled": false,
    "vlm_mtp_enabled": false,
    "specprefill_enabled": false
  }
}

几个要点:

  • dflash_block_size: 5 —— 量化模型官方建议不超过 5。
  • dflash_draft_model 必须是绝对路径。
  • enable_thinking 要同时写在 settingschat_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 预测)支持,但这个模型用不了,运行时给的原因很明确:

1
2
Config declares MTP layers but the weight files contain neither
mtp.* tensors nor native nextn layers.

config 里声明了 MTP 层,但权重文件里既没有 mtp.* 张量也没有原生 nextn 层——转换时丢了。官方 4-bit 版本同样如此。

七、客户端接入

任何 OpenAI 兼容客户端:

1
2
3
Base URL:  http://127.0.0.1:1234/v1
API Key:   本地 key
Model:     Qwen3.8-27B-Uncensored-MLX:code

冷启动需要载入 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 规划。