目标:在 Apple Silicon 上以最快速度推理 Qwen3.8-27B,支持长上下文与 Agent 工具调用,全部离线运行、不依赖外网 API。 更新日期:2026-09-06 | oMLX 0.6.4
1. 方案总览
方案路线:oMLX(官方 DMG v0.6.4,自带编译好的原生扩展)
├── 模型:Qwen3.8-27B-4bit(魔搭官方 4bit 主模型,E1)
├── 加速:Lightning MTP(draft depth 3,官方 MTP-4bit 头已合入主模型,E2)✅ 开
├── 加速:ANE Prefill(苹果神经引擎)❌ 关闭 —— 64GB 机型反噬(#3103),见 §10.3
├── 加速:TurboQuant KV(KV 4bit)❌ 关闭 —— 双开会掉速(#2215/#2782),见附2
└── 窗口:128K(131072,KV 按需增长,不预分配)
选型结论(对比后确定): - 框架:oMLX(官方 DMG)> MTPLX > llama.cpp+DFlash2 > Ollama(仅 6 tok/s,排除) - 模型:直接使用官方成品(4bit 主模型 + 4bit MTP 头),不再本地量化/拼装 - 加速:MTP 原生(零成本、兼容好);DFlash2 需额外草稿模型(未采用)
2. 硬件与系统要求
| 项 | 要求 | 本机实测 |
|---|---|---|
| 芯片 | Apple Silicon(M 系列) | M4 Max(16 核 CPU / 40 核 GPU) |
| 内存 | ≥ 48GB(推荐 64GB) | 64GB 统一内存 |
| 磁盘 | ≥ 40GB 可用 | 4TB SSD |
| 系统 | macOS 15+ | macOS 26.6.2 |
内存预算(64GB 机型,可用约 50GB): - 模型权重(官方 4bit):约 15.7GB - KV 缓存(全精度 @128K):按需增长,日常短对话仅 0.5-2GB;见 §7.3 实测(128K 在 64GB 可完整运行) - 系统留白:12-15GB
3. 软件环境与依赖
✅ 采用官方 DMG 安装(v0.6.4):自带预编译原生扩展(ANE/Metal kernel),无需手动编译、无需 venv/pip。早期源码安装版(/Users/light/omlx)已废弃。
3.1 安装(已完成)
- 从 oMLX 官方下载
oMLX-0.6.4.dmg,拖入 /Applications - 安装后 CLI 自动软链到
~/.omlx/bin/omlx→/opt/homebrew/bin/omlx(版本 0.6.4) - 模型目录:
~/.omlx/models/(settings.json 的model.model_dirs)
3.2 启动方式(终端)
# 方式一(推荐):托管后台服务(launchd 常驻,开机自启)
omlx start
omlx stop
omlx restart
# 方式二:前台运行(终端占用,Ctrl+C 退出)
omlx serve
# 验证服务是否在跑
curl http://127.0.0.1:8000/v1/models
⚠️ 端口被占用导致
omlx restart失败(Port 8000 is in use by PID xxx):常见于曾用omlx serve或命令行前台起过服务、残留了孤儿进程(如 python3.1)。清理命令:# 方法1:按 PID 杀(上面报错里的 PID,每次会变) kill <PID> # 方法2:按端口自动找(最省事,推荐) lsof -ti:8000 | xargs kill # 杀完验证端口已释放(无输出 = 释放成功),然后即可 omlx restart lsof -nP -iTCP:8000 -sTCP:LISTEN⚠️ 重启后的冷启动:重启会清空热缓存,SSD 缓存索引需重新加载。重启后第一次请求会全量 prefill(TTFT ~30s),跑完一轮后缓存链重建(命中 8192 tokens),后续每轮 TTFT 恢复 1~3s(见 §12)。
⚠️ 常见误解:不用再单独"启动"。服务由 DMG 托管(
auto_start_on_launch: true),开机/启动 App 即自动加载默认模型。请求必须带 API key(settings.json →auth.api_key),不带 key 会返回{"error":"API key required"}——这是鉴权提示,不代表服务没启动。检测服务状态用omlx start(幂等)或直接 curl。
服务启动后:OpenAI 兼容 API 于 http://127.0.0.1:8000(默认模型 Qwen3.8-27B-4bit 自动加载)
4. 模型选型与下载
4.1 最终采用(两个官方成品,直接下载部署)
| 文件 | 内容 | 大小 | 来源(魔搭,国内直连) |
|---|---|---|---|
| E1 | Qwen3.8-27B-4bit 主模型(官方 4bit,3 个分片) | ~15.6GB | https://modelscope.cn/models/mlx-community/Qwen3.8-27B-4bit |
| E2 | Qwen3.8-27B-MTP-4bit 官方 MTP 头(4bit affine g64) | 238MB | https://modelscope.cn/models/mlx-community/Qwen3.8-27B-MTP-4bit |
说明:不再采用 oQ4/FP8/mxfp4 拼装或反量化方案(早期路线已废弃);MTP 头无需反量化,直接合入主模型(见 §5)。
4.2 下载清单
本目录下已生成纯链接清单文件(每行一条,可导入迅雷批量下载):
| 清单 | 内容 |
|---|---|
| 下载清单-E1-官方4bit主模型.txt | mlx-community/Qwen3.8-27B-4bit 全部文件 |
| 下载清单-E2-官方MTP头.txt | mlx-community/Qwen3.8-27B-MTP-4bit 全部文件 |
4.3 放置目录
~/.omlx/models/Qwen3.8-27B-4bit/
├── config.json / tokenizer.json / model.safetensors.index.json ...
├── model-00001-of-00003.safetensors ~ model-00003-of-00003.safetensors
└── model-mtp.safetensors ← MTP 头(E2 合并产物,见 §5,文件名以 model. 开头才会被加载)
5. MTP 头合入主模型 + 模型配置(2026-09-06 最终版)
5.1 为什么必须合入主模型
外部 drafter 模式(vlm_mtp_enabled)在 prompt >2K 接受率骤降、>16K 直接早停(0 输出)。改为内嵌 Lightning MTP(mtp_enabled)后长上下文不再早停、decode 稳定不掉速。合入 = 把官方 MTP 头的权重以根级 mtp.* 前缀写进主模型目录 + config 声明存在。oMLX 0.6.4 原生支持(VLM 加载时 _remap_root_mtp_weights 自动映射到 language_model.mtp.*,sanitize 自动做 norm +1 shift)。
5.2 合并操作(已完成,可复现)
前提:主模型与 MTP 头量化格式必须一致(本方案 E1/E2 均为 4bit affine g64)。脚本 /tmp/merge_mtp.py 核心逻辑:
from safetensors.torch import load_file, save_file
t = load_file("E2/model.safetensors") # 官方 MTP-4bit 头,31 个张量
save_file({f"mtp.{k}": v for k, v in t.items()}, "E1/model-mtp.safetensors")
# 更新 E1/model.safetensors.index.json:weight_map 加 31 条 mtp.* → model-mtp.safetensors
# 更新 E1/config.json:text_config.mtp_num_hidden_layers = 1
关键点:
- 键名加根级 mtp. 前缀(mtp.fc.*、mtp.layers.0.*、mtp.norm.*、mtp.pre_fc_norm_*),与官方 MTP-4bit 成品格式一致
- torch 读写保留 BF16 dtype(numpy 会降成 float16)
- 合并后 E1/model-mtp.safetensors(239MB)与原 3 个分片共存,index.json 注册
5.3 配置(model_settings.json)
~/.omlx/model_settings.json 中为 Qwen3.8-27B-4bit(VLM 主模型)写入:
{
"Qwen3.8-27B-4bit": {
"max_context_window": 131072,
"mtp_enabled": true,
"mtp_num_draft_tokens": 3,
"turboquant_kv_enabled": false,
"qwen35_ane_prefill_enabled": false,
"is_pinned": true,
"is_default": true
}
}
字段说明:
| 字段 | 值 | 作用 |
|---|---|---|
| mtp_enabled | true | 内嵌 Lightning MTP(权重已合入主模型) |
| mtp_num_draft_tokens | 3 | 每次预测 3 个候选 token(官方推荐 k=3) |
| turboquant_kv_enabled | false | 双开会掉速(#2215/#2782),本场景无收益 |
| qwen35_ane_prefill_enabled | false | 64GB 上关闭(长上下文 ANE banks ~13GB 与 KV 争内存触发 shed + 节流,官方 PR #3103 结论) |
| max_context_window | 131072 | 128K 窗口(KV 按需增长不预分配) |
⚠️ 已知坑:MTP 与 TurboQuant KV 双开掉速(verify 加速失效、混合路径最慢,#2215/#2782)→ 默认关。
6. 启动与验证
omlx serve
6.1 验证模型加载
curl http://127.0.0.1:8000/v1/models
6.2 OpenAI 兼容接口测试(推荐:Python 干净流式)
curl 的流式输出是 SSE 协议原文(
data: {...}分片),可验证协议但不直观;日常用下方 urllib 脚本逐字打印最清楚(已实测可用)。
import json, sys, urllib.request
req = urllib.request.Request(
"http://127.0.0.1:8000/v1/chat/completions",
data=json.dumps({"model": "Qwen3.8-27B-4bit",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 2000, "stream": True}).encode(),
# 想跳过思考直接出正文,可加 "thinking_budget": 0
headers={"Content-Type": "application/json",
"Authorization": "Bearer omlx-9e479d0642791308e7ab76d4b763d12bc7239eb5bea09c46"})
for raw in urllib.request.urlopen(req, timeout=300):
line = raw.decode().strip()
if not line.startswith("data:"):
continue
if line[5:].strip() == "[DONE]":
break
delta = json.loads(line[5:])["choices"][0]["delta"]
if delta.get("reasoning_content"): # 思考 → 灰色
print("\033[2m" + delta["reasoning_content"] + "\033[0m", end="", flush=True)
if delta.get("content"): # 正文 → 正常色
print(delta["content"], end="", flush=True)
print()
⚠️ 说明:① 用标准库(urllib)而非
openaiSDK——oMLX 把思考放在reasoning_content字段流式输出,openai SDK 不解析该字段会丢正文(已实测);② 思考期间服务只发空 keepalive,脚本把思考也打出来(灰色)就不会显得"没反应";③ 想跳过思考直接出正文,给 JSON 加"thinking_budget": 0;④ 终端测试也可用python3 ~/.omlx/test_chat.py(同款脚本)。
整段输出测试(非流式 curl,一次性返回完整 JSON):
不带
stream时会等整段生成完才一次性输出——长回复要等几十秒~几分钟,期间终端看似"卡住无响应",属正常。
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer omlx-9e479d0642791308e7ab76d4b763d12bc7239eb5bea09c46" \
-d '{"model":"Qwen3.8-27B-4bit","messages":[{"role":"user","content":"你好"}],"max_tokens":100}'
流式 SSE 原文(可选,输出为 data: {...} 分片属正常):见 §11.2 curl 流式示例。
6.3 速度基准(M4 Max 64GB,2026-09-06 内嵌 Lightning MTP 最终实测)
| 配置 | 纯 decode | 说明 |
|---|---|---|
| 内嵌 Lightning MTP(mtp_enabled,当前配置) | 44.6 tok/s(0.5K) | ✅ 官方 4bit + MTP-4bit 头合入 |
| 长上下文 16K/32K decode | 42.2 / 41.9 tok/s | ✅ 不掉速(见 §7) |
✅ 2026-09-06 最终结论:官方 4bit 主模型 + 官方 MTP-4bit 头合入主模型后(内嵌 Lightning MTP),M4 Max 64GB 实测 44.6 tok/s(短上下文),长上下文 16K 42.2 / 32K 41.9 tok/s(对比 Weschera 128GB 实测 53.3,差距 ~15%)。长上下文不再早停,接受率 57~88%。 服务日志确认:
Remapped 31 root mtp.* weight(s) to language_model.mtp.*+Speculative backend selected: Lightning MTP (model_type=qwen3_5, active)。
7. 上下文与内存策略(2026-09-06 实测)
7.1 不同上下文长度实测(M4 Max 64GB,内嵌 Lightning MTP,2026-09-06 最终)
A. 摊销速度(含 prefill,非 stream,输出 120 tokens):
| prompt 长度 | 输出 tokens | 总耗时 | 摊销 tok/s | finish |
|---|---|---|---|---|
| 57 | 120 | ~4.5s | 26.6 | length |
| 3,017 | 120 | 14.8s | 8.1 | length |
| 5,977 | 120 | 10.8s | 11.1 | length |
| 11,897 | 120 | 18.4s | 6.5 | length |
摊销速度低是 prefill 时间占大头(8K prompt prefill ~12s),decode 本身很快(见 B),大 prompt 的 TTFT 是所有 LLM 服务的正常现象。
B. 纯 decode 速度(时间差法:同 prompt 两次 max_tokens 求差,剔除 prefill):
| prompt 长度 | 纯 decode tok/s |
|---|---|
| 57(0.5K) | 44.6 |
| 3,017(8K) | 49.2 |
| 5,977(16K) | 42.2 |
| 11,897(32K) | 41.9 |
✅ 最终结论:内嵌 Lightning MTP 后,decode 稳定 42~49 tok/s,长上下文(8K~32K)不掉速、不早停。MTP 接受率 57~88%(tok/cycle 最高 3.0),与 Weschera 128GB 实测(prose 53.3)差距 ~15%。 实用配置:agent 任务无需关 MTP——内嵌模式长上下文天然稳定,短/长任务统一开 MTP 即可。
7.2 prefill(TTFT)速度——长上下文的等待瓶颈
| prompt 长度 | TTFT(首 token) | prefill 吞吐 |
|---|---|---|
| 57 | ~0.6s | ~250 tok/s |
| 3,017 | ~11.9s | ~250 tok/s |
| 5,977 | ~8.3s | ~250 tok/s |
| 11,897 | ~15.7s | ~250 tok/s |
prefill 走 GPU 路径恒定 ~250 tok/s(已含 0.6.4 QSA/稀疏注意力优化,接近 Weschera ANE on 的 273 tok/s)。长上下文总等待时间由 prefill 主导。 throttle 调优:默认
prefill_priority: context时,长请求被 per_token 高估(13.7MB/token)触发 adaptive 节流(chunk 3016→2432)。改为scheduler.prefill_priority: "speed"(settings.json)后 chunk 不再收缩,节流消除。配合memory_guard_tier: aggressive(保留 admission 保护,64GB 无 OOM 风险)。配置变更记录(2026-09-07 04:11):
memory_guard_tieraggressive → balanced(超大 prefill 请求偶尔触发adaptive_prefill_throttlepause:改前约 8 次/4h、当前 target≈43GB 时大请求需 14GB+ headroom 被暂停等内存)。balanced 放宽暂停阈值,代价是 64GB 防 OOM 余量略降(guard 仍开)。对照基线(aggressive 期间):needs-prefill-headroom pause 全天 20 / 今早 3;Prefill throttled(chunk 降级)全天 9 / 今早 0;ANE shed 回退 3(佐证 64GB 关 ANE 正确)。重启后 server.log 轮转,跑一天后对比:grep -c "adaptive_prefill_throttle"、grep -c "Prefill throttled"、关注是否出现真 OOM/模型驱逐。
7.3 内存
- KV 缓存按需增长,窗口设大不白占内存;日常对话 KV 仅 0.5-2GB
- 32K 上下文实测总内存 ~20GB(模型 15.7 + KV + 系统),64GB 机型无压力
- full attention 下"长上下文"与"高速"不可兼得,这是 dense 模型物理规律
8. 常见问题(FAQ)
Q1:HF 链接打不开? HF 在国内被墙。方案改用魔搭直连(清单 E1/E2),无需梯子。若必须下 HF 原版,需加速器全局/TUN 模式覆盖 huggingface.co 与 cdn-lfs.huggingface.co。
Q2:迅雷导入清单会漏掉大文件? 迅雷批量导入有时忽略 safetensors 分片,需手动补充下载 model-0000X-of-00003.safetensors。
Q3:MTP 头必须叫什么名字?
model.mtp.safetensors——mlx-lm 按 model*.safetensors 通配加载,不带 model. 前缀不会被识别。
Q4:模型权重 key 前缀?
主模型为 VLM 包装器格式 language_model.*;MTP 头为根级 mtp.*,oMLX 0.6.1+ 会自动映射到语言模型 MTP 模块。
Q5:fp8 推理更快吗? 否。FP8 是量化原料不是推理格式;4bit(官方 4bit / oQ4e)推理最快。
Q6:Agent 每轮首 token 都几十秒,是模型慢吗? 不是推理慢(decode 已 40+ tok/s),是 oMLX 前缀缓存失配:dsh/Trae 请求的 tools 段(6.6K tokens)逐轮变化(哪怕顺序交换)导致前缀从 system+tools 交界处失配 → 每轮全量重算 prefill。解法见 §12.4(前缀稳定、tools 一致、会话长度控制)。同前缀请求实测 TTFT 可从 50s+ 降到 5s。
Q7:MTP draft depth 用 2 还是 3? 综合用 3(Weschera 同栈实测 prose 53.3 / code 72.1,k=3 综合最佳;k=4 仅 code +1.2、prose -5.4)。Agent 短输出场景 k=2 接受率更高(80.5% vs 71.1%),更稳。本机实测 k=3 44.4 vs k=2 ~40 tok/s,k=3 略优。
9. 后续演进(待办)
- 向量数据库长记忆:mlx-embeddings + 向量库存储历史对话,实现跨会话记忆(Agent 化最后一块)
- DFlash2 对比测试:dflash-mlx 已装,可实测 vs Lightning MTP 的速度差异(需额外草稿模型内存)
- MoE 备选:Qwen3.6-35B-A3B 长上下文不掉速场景对比
- 超长存档模式(按需临时):一次性读取超长文档时可临时调大窗口 + TurboQuant KV 4bit + 关 MTP,用毕切回 128K 高速配置
10. MTP 在 Mac M4 上的优化方法(oMLX 栈,2026-09-06 调研)
来源:Weschera M4 Max 实测配方 + M4 Max 生产实践 + oMLX 官方生态。同栈(oMLX)实测可达 prose 53 / code 72 tok/s(M4 Max,短上下文)。
10.1 优化杠杆(按收益排序)
| # | 优化项 | 收益(实测依据) | 状态 |
|---|---|---|---|
| 1 | MTP 头合入主模型(内嵌 Lightning MTP,官方 4bit 头) | 22→44.6 tok/s;长上下文不掉速不早停(核心) | ✅ 完成(见 §5) |
| 2 | ANE Prefill 引擎 | decode +10%;prefill 83→274 tok/s | ❌ 64GB 关闭:banks ~13GB 与 KV 争内存,长上下文触发 shed + 节流(官方 PR #3103 实测 64GB 反而更慢) |
| 3 | MTP draft depth = 3 | prose 53.3 / code 72.1(k=3 综合最佳;k=4 仅 code +1.2、prose -5.4;agent 短输出 k=2 接受率 80.5% 更稳)。本机实测 k=3 44.4 vs k=2 ~40 tok/s,k=3 略优 | ✅ 已配(2026-09-06) |
| 4 | oMLX 0.6.4 DMG | 含 QSA prefill、ANE shed、Lightning MTP 状态修复 | ✅ 已是 |
| 5 | TurboQuant KV 关闭 | 双开掉速(#2215/#2782);本场景 KV 按需增长无收益 | ✅ 已关 |
| 6 | prefill_priority: speed | 消除长请求 prefill 节流(per_token 高估 13.7MB/token 触发) | ✅ 已配 |
10.2 目标配置(model_settings.json)
✅ 已写入(见 §5.3):
mtp_enabled: true+mtp_num_draft_tokens: 3+qwen35_ane_prefill_enabled: false+max_context_window: 131072。
10.3 ANE Prefill 补装(64GB 机型结论)
- 不补装/保持关闭。oMLX 0.6.4 DMG 自带 ANE kernel(
/api/status显示qwen35_prefill: available: true,重启后 ANE 可正常配置 64 MLP + 48 GDN 层),但 64GB 上长上下文触发 shed:ANE banks 常驻 ~13GB,与 KV cache 争内存 → 请求被反复节流(日志Prefill throttled: chunk 2048 -> 512类),官方 PR #3103 在 64GB 实测 ANE on 反而更慢(217 vs 326 tok/s)。 - 128GB 机型可开启(Weschera 实测 +10% decode、prefill 3x),建议开启后跑本机 Tuner 定比例。
10.4 实测速度(M4 Max 64GB,2026-09-06 内嵌 Lightning MTP)
| 配置 | 纯 decode | 参考 |
|---|---|---|
| 无投机(4bit,MTP off) | 22.9 tok/s | 本机实测基线 |
| Lightning MTP(当前) | 44.6 tok/s(0.5K) | 实测 |
| 8K / 16K / 32K 上下文 | 49.2 / 42.2 / 41.9 tok/s | 实测,长上下文不掉速 |
| Weschera 128GB 同栈 | 53.3 tok/s | 社区实测(128GB 机型) |
✅ 长上下文不再需要关 MTP:内嵌 Lightning MTP 在 8K~32K 接受率 57~88%、decode 稳定不掉速(§7.1B)。
10.5 已知坑(勿重蹈)
- MTP 头不要手工反量化/转换:自制 FP8→BF16 头有 norm 权重损坏隐患 → 用官方成品(mlx-community/Qwen3.8-27B-MTP-4bit)后合入主模型(§5),不要用外部 drafter 模式(长上下文早停)
- MTP 草稿别量化过头:量化草稿会打掉接受率(fp16 草稿 15 → 量化草稿 12,白算一轮)
- TurboQuant KV 与 MTP 双开:verify 加速失效、实测掉速(#2215/#2782)→ 默认关;仅超长存档模式(§9 #4:临时关 MTP)才开
- 多并发批次:位置不对齐时 oMLX 回退普通解码,MTP 统计会显示"生效但无收益"
- 投机二选一:MTP 与 DFlash2 互斥(oMLX 校验拒绝同开)
- 64GB 别开 ANE prefill:长上下文 banks 竞争内存触发 shed + 节流(§10.3)
11. 使用指南(启动 / 访问 / 接入其他 Agent)
11.1 启动服务
# 托管后台服务(launchd 常驻,当前使用方式,改配置后重启生效)
omlx restart
# 停止 / 启动
omlx stop
omlx start
# 前台调试(终端占用)
omlx serve
# 验证
curl http://127.0.0.1:8000/v1/models
服务由 DMG 托管,开机/启动 App 自动加载默认模型
Qwen3.8-27B-4bit(model_settings.json 的is_pinned/is_default)。
11.2 访问对话(OpenAI 兼容 API)
当前服务已开启 API key 鉴权(settings.json → auth.api_key),所有请求需带:
Authorization: Bearer omlx-9e479d0642791308e7ab76d4b763d12bc7239eb5bea09c46(API key 存于~/.omlx/settings.json的auth.api_key,可从设置界面改)
API_KEY="omlx-9e479d0642791308e7ab76d4b763d12bc7239eb5bea09c46"
# 非流式
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{"model":"Qwen3.8-27B-4bit","messages":[{"role":"user","content":"你好"}],"max_tokens":200}'
# 流式(打字机效果)
curl -N http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{"model":"Qwen3.8-27B-4bit","messages":[{"role":"user","content":"写一首诗"}],"stream":true}'
Python / OpenAI SDK:
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8000/v1",
api_key="omlx-9e479d0642791308e7ab76d4b763d12bc7239eb5bea09c46",
)
resp = client.chat.completions.create(
model="Qwen3.8-27B-4bit",
messages=[{"role": "user", "content": "你好"}],
max_tokens=200,
)
print(resp.choices[0].message.content)
11.3 配置其他 Agent 接入(OpenAI 兼容)
所有支持 OpenAI 兼容 API 的工具统一填:
| 配置项 | 值 |
|---|---|
| API Base URL | http://127.0.0.1:8000/v1 |
| API Key | omlx-9e479d0642791308e7ab76d4b763d12bc7239eb5bea09c46 |
| 模型名 | Qwen3.8-27B-4bit |
常见工具:
- Cline / Roo Code(VSCode):Provider 选 "OpenAI Compatible",Base URL 填 http://127.0.0.1:8000/v1,Model ID 填 Qwen3.8-27B-4bit,API Key 任意
- Continue(VSCode/JetBrains):config.yaml 添加 openai provider,apiBase: http://127.0.0.1:8000/v1,model: Qwen3.8-27B-4bit
- Cherry Studio / ChatBox / LobeChat:自定义 OpenAI 服务,地址同上
- Open WebUI:环境变量 OPENAI_API_BASE_URL=http://127.0.0.1:8000/v1
- Claude Code / Codex 类 CLI:ANTHROPIC_BASE_URL 类配置指向 http://127.0.0.1:8000(oMLX 兼容 Anthropic SDK,支持 x-api-key 头)
长上下文任务(>2K prompt)无需关 MTP:内嵌 Lightning MTP 在 8K~32K 接受率 57~88%、decode 稳定不掉速(§7.1B)。
12. 前缀缓存与首 token(TTFT)调优(2026-09-06 实测,Agent 场景必读)
症状:dsh / Trae 等 Agent 每轮请求首 token 都几十秒(TTFT 32~53s),缓存命中率 0~2%。这是 Agent 场景下 oMLX 缓存失配问题,与推理速度无关(decode 已 40+ tok/s)。
TTFT 慢的三层根因(2026-09-06 定论,先看这个再往下读):
| 层 | 原因 | 能否优化 | 手段 |
|---|---|---|---|
| ① 物理层 | 长 prompt 全量 prefill 是纯算力:Qwen3.8-27B dense 在 M4 Max prefill 恒定 ~250 tok/s,8K prompt 就要 ~12s | ❌ 无法消除(所有 LLM 通病) | 无 |
| ② 缓存失配层 | 前缀缓存按 token 精确匹配、4096 粒度;Agent 请求前缀 system+tools+历史动辄 8~13K,任一处变化 → 变化点后全量重算 | ✅ 主要优化对象 | 命中后只重算新增增量,TTFT 33s → 1~3s |
| ③ 环境干扰层 | 重启冷启动(首轮无缓存 ~30s)、客户端超时中断 prefill、误配 ANE/TurboQuant 触发节流 | ✅ 可消除 | 见 §12.4 / §10 |
一句话:模型本身不慢(decode 42+ tok/s),长 prompt 的 prefill 时间是物理下限;真正能优化的是让前缀缓存命中,命中后每轮只算新增几百 tokens,TTFT 即 1~3s。
12.1 oMLX 缓存机制(实测确认)
- 两级缓存:hot cache(内存,本机 8GB)+ SSD cache(
~/.omlx/cache,本机 7.19GB) - 匹配规则:按 prompt token 前缀精确匹配,粒度为 block_size=4096 的完整块(混合模型 ArraysCache 架构限制)
- tools 敏感:请求的
tools参数会被渲染进 prompt 前缀(system 之后)。tools 的任何变化(哪怕仅顺序交换)→ 前缀从该处失配 → 全量重算
12.2 决定性实验(本机,oMLX 0.6.4 + Qwen3.8-27B-4bit)
| 请求 | 结果 |
|---|---|
| 相同请求(冷) | 命中 0,全量 prefill 16.7K tokens,68.6s |
| 相同请求(第 2 次) | 命中 16,384,仅重算 322 tokens,2.5s |
| 仅 tools 顺序交换 | 命中 0,全量 81.4s |
| 稳定前缀 + 逐轮增长(模拟 Agent) | 命中 12,288,TTFT 54s → 5~12s |
结论:oMLX 缓存本身工作正常;Agent 每轮慢 = 请求前缀(尤其 tools 段)逐轮漂移导致失配。
12.3 Agent(dsh)真实请求结构(抓包实测)
[system 1,717 tokens] + [tools 6,611 tokens / 26 个工具] + [messages 4,826 tokens]
↑ 中段最大的一块,漂移高发区
实测案例:同一 dsh 会话 9 轮累计仅命中 4,096 tokens(1 个块)——前缀在 system+tools 交界处即开始失配。dsh 的请求:stream: true、不传 temperature(oMLX 默认)、tools 作为 OpenAI tools 参数发送。
12.4 Agent 场景缓存优化清单
- 前缀稳定第一:system prompt 静态化——动态内容(时间、cwd、路径)放尾部,前 8K 保持逐轮一致
- tools 逐轮一致:勿动态增删工具、勿重排工具顺序(实测顺序交换即全量失配)
- 会话长度控制:用历史压缩/截断让 prompt 总量稳定在 ~8K,命中 8,192 后每轮仅重算增量(TTFT 2~5s)
- 客户端超时调大:prefill 被超时中断 → 该轮缓存不落盘 → 下轮全量重来(恶性循环)
- 避免 exact-hit 回退:oMLX 对"完全命中"(prompt 与缓存序列完全相等)的 stateful 缓存会回退全量 prefill(确定性需求)——agent 每轮应保证有新增尾部,而不是重发相同 prompt
12.5 已知限制
- 缓存写入依赖边界快照:
boundary_snapshot_unavailable时跳过写入(MTP warm-prefix 恢复后 available_boundaries=0,良性但缓存停在旧边界不增长) - SSD 索引重启加载不稳定:曾出现
scanned=0, indexed=0(缓存成了死数据),重启后可能需首次请求重新预热 - 缓存命中率可查:面板"缓存效率"(本机曾 44.5%),或 API usage 的
prompt_tokens_details.cached_tokens
附:当前环境快照(2026-09-06 最终)
硬件:Mac Studio M4 Max(16C CPU / 40C GPU / 64GB)
系统:macOS 26.6.2
oMLX:0.6.4(官方 DMG,/Applications/oMLX.app,CLI 软链 ~/.omlx/bin/omlx)
服务:127.0.0.1:8000(OpenAI 兼容,launchd 托管常驻,鉴权已开,API key 见 §11.2)
默认模型:Qwen3.8-27B-4bit(官方 4bit,15.7GB,E1)+ 官方 MTP-4bit 头(E2,已合入,238MB)
配置:128K 窗口 / MTP depth 3 / TurboQuant KV 关 / ANE 关 / memory guard balanced(2026-09-07)
实测:短上下文纯 decode 44.6 tok/s;长上下文 8K~32K 稳定 42~49 tok/s,不掉速不早停
已修复:mlx-vlm gdn_states=None 崩溃(qwen35_vlm_runtime.py 容错 patch)
附2:ANE / TurboQuant「关闭」结论的证据等级(2026-09-07 澄清)
背景:总览 L14-15 与 §10.1 #2/#5 写「关闭」。以下如实区分哪些是官方/社区同机实测、哪些是本机旁证、哪些是本机尚未做的 A/B,避免把"无副作用"误读成"已证明更有益"。
| 结论 | 依据来源 | 证据等级 |
|---|---|---|
| 关 ANE(64GB 反噬) | 官方 PR #3103 在 M5 Pro 64GB 同机 A/B:57K prompt 下 ANE on 217/177 tok/s(每次触发 chunk 2048→512 收缩) vs off 326/300 tok/s(从不节流);16K 时 +13% 为正、随上下文深度转负 |
别机同机 A/B,本机未做 on/off 对照 |
| 关 ANE(本机旁证) | 本机 2026-09-07 04:11 前(aggressive)日志 ANE shed 回退 3 次——说明开启后确会与长上下文 KV 争内存被挤掉 |
本机日志旁证 |
| 关 TurboQuant | ① 0.6.2+ 双开不再崩溃但 MTP verify 加速失效回退(#2782);② 混合路径实测最慢(#2215,他机);③ vLLM/Red Hat 研究:KV 量化 3bit 明显掉点、4bit 适度掉点 | 他机/官方研究,本机未做 on/off 对照 |
| guard balanced | 04:11 aggressive→balanced 时序对比:needs-prefill-headroom pause 8 次/4h → 重启后观察窗 0 次;代价为防 OOM 余量略降(guard 仍开) | 本机时序对比(非同机 A/B) |
2026-09-07 全关组合本机实测(证明"该组合可行、无副作用"): - 纯 decode:散文 47.0、列表/高可预测文本 54.5 tok/s(14.7K 上下文,MTP accept 66~93%) - 长上下文质量:14.7K 文档定向检索正确(第106号档案);逐行复述前 105 行内容全对(截断为 max_tokens 配额所致) - 缓存:第二轮命中 12,288/14,788,prefill 60s→11s - throttle:重启后观察窗 0 次(含 balanced 生效) - 待澄清:上述只证明"关闭可行",不证明"开启更差"——本机 A/B 未做
可选 A/B 复现(日后验证时更新本节):
- TurboQuant A/B:model_settings.json 临时 turboquant_kv_enabled: true → omlx restart → 同 prompt 测 decode/verify 是否回退 → 还原
- ANE A/B:临时 qwen35_ane_prefill_enabled: true → restart(需编译 ~13GB banks)→ 测长 prompt throttle 次数与 prefill tok/s → 还原
试用结论:保持现状(ANE 关 / TurboQuant 关 / guard balanced)作为日常基线;若后续出现长上下文任务变多的场景,再按上表跑本机 A/B 决定是否调整。
举手提问