目标:在 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 安装(已完成)

  1. 从 oMLX 官方下载 oMLX-0.6.4.dmg,拖入 /Applications
  2. 安装后 CLI 自动软链到 ~/.omlx/bin/omlx/opt/homebrew/bin/omlx(版本 0.6.4)
  3. 模型目录:~/.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 MTPmtp_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)而非 openai SDK——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_tier aggressive → balanced(超大 prefill 请求偶尔触发 adaptive_prefill_throttle pause:改前约 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. 后续演进(待办)

  1. 向量数据库长记忆:mlx-embeddings + 向量库存储历史对话,实现跨会话记忆(Agent 化最后一块)
  2. DFlash2 对比测试:dflash-mlx 已装,可实测 vs Lightning MTP 的速度差异(需额外草稿模型内存)
  3. MoE 备选:Qwen3.6-35B-A3B 长上下文不掉速场景对比
  4. 超长存档模式(按需临时):一次性读取超长文档时可临时调大窗口 + 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 已知坑(勿重蹈)

  1. MTP 头不要手工反量化/转换:自制 FP8→BF16 头有 norm 权重损坏隐患 → 用官方成品(mlx-community/Qwen3.8-27B-MTP-4bit)后合入主模型(§5),不要用外部 drafter 模式(长上下文早停)
  2. MTP 草稿别量化过头:量化草稿会打掉接受率(fp16 草稿 15 → 量化草稿 12,白算一轮)
  3. TurboQuant KV 与 MTP 双开:verify 加速失效、实测掉速(#2215/#2782)→ 默认关;仅超长存档模式(§9 #4:临时关 MTP)才开
  4. 多并发批次:位置不对齐时 oMLX 回退普通解码,MTP 统计会显示"生效但无收益"
  5. 投机二选一:MTP 与 DFlash2 互斥(oMLX 校验拒绝同开)
  6. 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.jsonauth.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/v1model: 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 类 CLIANTHROPIC_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 场景缓存优化清单

  1. 前缀稳定第一:system prompt 静态化——动态内容(时间、cwd、路径)放尾部,前 8K 保持逐轮一致
  2. tools 逐轮一致:勿动态增删工具、勿重排工具顺序(实测顺序交换即全量失配)
  3. 会话长度控制:用历史压缩/截断让 prompt 总量稳定在 ~8K,命中 8,192 后每轮仅重算增量(TTFT 2~5s)
  4. 客户端超时调大:prefill 被超时中断 → 该轮缓存不落盘 → 下轮全量重来(恶性循环)
  5. 避免 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: trueomlx 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 决定是否调整。