Mac Studio 本地跑 Qwen 122B:修好 3 个缓存 Bug 后的 qMLX 实战
1. 为什么 96GB 统一内存跑 Qwen 122B 会卡在首 token先说结论模型能装进 Mac Studio 的统一内存不代表它就能当聊天机器人用。我见过太多人把 122B 级别的 MoE 权重下载完、加载成功、看到第一句回复就以为大功告成。真正的坑在第二轮对话才开始暴露——你追问一句光标转三分钟第一个字才慢悠悠冒出来。这不是模型慢是缓存路径没走通。Qwen 122B 这类混合注意力模型结构上把 GatedDeltaNet一种 SSM 循环层和稠密注意力层混在一起。SSM 的循环状态有个要命的特性它没法像普通 KV 块那样裁切或回退到更早的位置。于是很多推理栈为了不内存泄漏干脆把任何包含 SSM 层的缓存条目全丢掉。结果就是内存前缀缓存几乎永远 miss每一轮对话都从头重算整段上下文。我在一台 M3 Ultra、96GB 统一内存的 Mac Studio 上实测13 万 token 的对话窗口里内存命中 0 次磁盘命中 109 次。也就是说唯一让模型保持温热的是把 attention KV checkpoint 到 SSD下一回合再 restore 回来。磁盘恢复不是备胎它就是整个缓存系统本身。而它一直在坏三种坏法一种藏在另一种后面。这篇要解决的就是这件事在 Mac Studio 统一内存环境下用 qMLX 部署 Qwen 122B 混合注意力模型把 KV 缓存复用、分页缓存、量化缓存这三类 Bug 逐一复现并修掉。你会拿到可复制的启动参数、缓存配置片段、逐项验证命令以及修复前后的显存占用和首 token 延迟对比。适合谁手上有一台大内存 Mac、想本地跑长上下文 Agent 编程、被冷 prefill 折磨过的开发者。qMLX 是 rapid-mlx 的一个 fork专门面向 Apple Silicon 上的混合 Qwen 模型核心就是磁盘 KV restore 子系统。基础引擎、OpenAI/Anthropic API 面、MLX serving 路径来自 rapid-mlxqMLX 加的是混合感知磁盘恢复、驱逐策略、分阶段 metrics 和 Qwen 专门化。下面所有数字都来自同一台机器M3 Ultra28 核 CPU20 性能 8 能效60 核 GPU96GB 统一内存macOS 26.4。这是 qMLX 唯一测过的配置阈值请当作这台机器的实测。2. 三个缓存 Bug 的复现路径与 qMLX 修复思路在动手配环境之前得先搞清楚这三个 Bug 分别长什么样否则你照着参数跑起来遇到冷填充也只会以为是模型不行。我把复现和修复拆开讲每一步都对应一个可观察的现象。Bug 一系统提示里的时间戳。KV 复用要求字节级完全一致提示词改一个字符匹配就在第一处差异失败之后全部重算。很多 Agent 框架会在每一轮往系统提示里写一个唯一的 message ID。这个唯一值出现在 13 万 token 提示的靠前位置意味着提示从未字节稳定第二轮和第一轮在前几百 token 内就不同了。缓存的 Agent 上下文被扔掉整段系统提示重建匹配早早发散每一轮都是冷启动。修复很简单删掉那一行。message ID 只是装饰没有代码读回它Agent 本来就在每轮 user 消息里带 ID。通用规则是——任何「每轮唯一」的东西都不该进可缓存前缀应该放在本来就要变的那一段。Bug 二从未落盘的回复。系统提示修好后撑了一段时间又在对话更深处崩溃。如果你在模型还在回复时发送新消息Agent 会中断当前生成这是正确行为。但中断路径里代码直接 break没有保存已经流式输出的回复。推理端已经把这些 token 写进 KV历史里却缺了 assistant 这一轮。发散点在对话深处又是冷填充。我在数据库里证实连续四条用户消息之间没有 assistant 回合而屏幕上明明已经流式显示过的回复不在历史里——从未写入。修复在中断路径上先持久化已流式内容再 break和网络断流时已有的恢复逻辑保持一致。通用规则是——只要某次生成的 token 能进服务端缓存这次生成就必须在所有退出路径上提交到历史包括各种狼狈的中断。Bug 三checkpoint 仓库里的「毒药」。前两个修完后缓存可以一轮接一轮保持温热却在任何使用工具或被中断的回合上恰好冷掉一次然后又恢复。原因是有两个写入者碰 checkpoint 仓库一个写真货按 prompt 键入、下一回合要 restore 的那份另一个是后台钩子每生成 256 token 写一次完整 checkpoint且没有 token 键永远无法匹配或恢复纯死重还占磁盘配额。一次长工具调用会生成大量 token触发大量垃圾写入把仓库顶过容量上限驱逐策略按最旧删除把好 checkpoint 和垃圾一起干掉。当时磁盘上单个目录 27GB 不可匹配体挤掉真正重要的 checkpoint。修复让驱逐优先删不可匹配项在开启 restore 时彻底关掉垃圾写入器。好 checkpoint 活下来下一回合能 restore冷填充停止。这三个 Bug 的共同点是它们都不抛异常只是让缓存静默失效。你看到的现象永远是「首 token 很慢」但根因在三个完全不同的地方。qMLX 的设计原则就是围绕这个来的——混合注意力与 DeltaNet 是一等公民循环状态不能像 KV 块那样裁切缓存路径必须显式处理SSD 缓存流是一级存储不是备胎缓存路径正确性优于聪明错误 restore 不抛异常会腐蚀状态。3. 可复制的 qMLX 启动参数与缓存配置片段这一节是全文最该抄的部分。我先把环境准备、模型加载、缓存配置、启动命令按顺序给全路径和原文保持一致你照着改机器名就能跑。首先是依赖和仓库。qMLX 是 rapid-mlx 的 fork安装方式沿用 MLX 生态git clone https://github.com/marzukia/qMLX.git cd qMLX python -m venv .venv source .venv/bin/activate pip install -e .模型权重建议放在 NVMe 上别放外置机械盘磁盘 restore 的吞吐直接决定首 token 延迟。以 Qwen 122B 低比特量化版为例权重目录假设为/models/qwen-122b-mlx-4bit。接下来是缓存配置。qMLX 的缓存配置走一个 JSON 文件我把它放在~/.qmlx/cache_config.json内容如下{ cache: { memory_prefix_cache: false, disk_kv_cache: true, disk_cache_dir: /nvme/qmlx-kv, disk_cache_max_gb: 200, checkpoint_interval_tokens: 0, eviction_policy: unmatchable_first, restore_guard_enabled: false, token_blob_checksum: true }, model: { path: /models/qwen-122b-mlx-4bit, hybrid_attention: true, deltanet_state_aware: true } }几个关键项解释一下。memory_prefix_cache设为 false 是因为混合注意力下内存前缀缓存结构上就是死的开着只会浪费内存。disk_kv_cache必须 true这是整个系统的命脉。checkpoint_interval_tokens设为 0意思是关掉那个每 256 token 写垃圾的后台钩子对应 Bug 三的修复。eviction_policy用unmatchable_first驱逐时优先删不可匹配项保住好 checkpoint。restore_guard_enabled暂时关掉因为当前 guard 估计过于保守会拒绝本可成功的 restore这个后面排障会讲。token_blob_checksum打开字节校验 token blob隔离坏 checkpoint。然后是启动命令。qMLX 的 serve 入口沿用 rapid-mlx 的参数风格qmlx serve \ --model /models/qwen-122b-mlx-4bit \ --cache-config ~/.qmlx/cache_config.json \ --host 127.0.0.1 \ --port 8080 \ --max-context 200000 \ --hybrid-attention \ --disk-kv-restore \ --metrics-port 8081如果你要接 Claude Code 或 Cline 这类客户端Base URL 填http://127.0.0.1:8080/v1API Key 随便填一个非空字符串即可Model ID 填qwen-122b。这三件套缺一不可很多人只填 Base URL 就报 401其实是 Key 没给。启动后先别急着对话用一条唯一 prompt 打一次确认缓存路径真的在工作curl -s http://127.0.0.1:8080/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer local \ -d { model: qwen-122b, messages: [{role: user, content: cache probe 2024-11-05-001}], max_tokens: 8 }再发一次同样的请求第二次应该看到cached字段非零、prefill字段骤降。如果两次 prefill 一样大说明缓存没命中回到配置检查disk_kv_cache和disk_cache_dir权限。4. 验证请求与修复前后的首 token 延迟对比配置跑起来只是第一步真正要确认的是缓存命中率。qMLX 在 8081 端口暴露了 metrics我习惯用一条命令盯住关键指标curl -s http://127.0.0.1:8081/metrics | grep -E cache_(hit|miss)|prefill_tokens|restore修复前同一段 13 万 token 的对话每一轮都是冷填充 3 万 token 起步。日志长这样uid58 MISS cached0 prefill31240 uid59 MISS cached0 prefill31502 uid60 MISS cached0 prefill31877修复后同一对话从 3.1 万长到 5.7 万 token每一轮都 restore 上一轮上下文只对新消息做 prefilluid58 HIT cached53267 prefill670 uid59 HIT cached54009 prefill33 uid60 HIT cached54113 prefill1671 uid61 HIT cached55867 prefill45 uid62 HIT cached55996 prefill1869曾经分钟级的地方变成亚秒级。checkpoint 目录也干净下来条条可匹配没有垃圾。再说显存占用。修复前因为内存前缀缓存反复 miss、磁盘仓库被垃圾顶爆KV 在内存和磁盘之间来回搬统一内存的峰值占用经常顶到 88GB 以上系统开始压缩内存风扇起飞。修复后内存前缀缓存关掉、磁盘 restore 稳定命中统一内存峰值回落到 62GB 左右留出余量给系统和 Agent 框架本身。这个余量很关键因为 96GB 不是全给模型的。首 token 延迟的对比更直观。同一重复 prompt开缓存与关缓存的 prefill 时间秒越低越好关缓存时重复 3.2 万 token 的 prompt 每次仍要 88 秒 prefill开缓存后 0.64 秒。差距随上下文变长而拉大1k 时约 13 倍32k 时约 137 倍。但这里有个必须说清的限定restore 不等于免费。很深的一轮里缓存从 SSD 直接喂掉 99% 以上的 prompt所以首 token 时间跟踪的是增量——自上次 checkpoint 以来的新 token而不是整段 prompt。坐在 168k token 时一句短追问可能只有 67 个新 tokenTTFT 2.6 秒其余 168,373 来自磁盘。但增量仍按全价 prefill在这个深度每个新 token 都要 attend 整个 165k KV大约 10ms/token。一行问题仍然快在对话深处粘贴大段工具结果或文件1800 新 token 可能又回到 17 秒才见首 token。restore 消灭了冷 prefill 悬崖并没有让深上下文 prefill 免费——越深每个增量 token 越贵。decode 吞吐也值得单独看。短上下文约 55 tok/s64k 仍约 28 tok/s。这条曲线是 25% 稠密注意力层的可见代价每生成一个 token 都要重读整段 KV读量随上下文增长。另外 75% DeltaNet 层携带常量大小的循环状态几乎不随上下文变慢所以是 64 倍上下文长度下约 2 倍变慢而不是断崖。混合设计在这里不是妥协而是让长上下文 decode 仍可用的关键。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth跑 qMLX 的过程中我踩过的报错基本集中在四类。逐个说清楚现象、根因和修法。401 Unauthorized。现象是客户端连上 8080 端口但每次请求都返回 401。根因几乎都是 API Key 没填或填了空字符串。qMLX 本地服务默认要求 Authorization 头非空但值本身不校验。修法在客户端里把 API Key 填成任意非空字符串比如local。如果你用的是 Claude Code 或 Cline记得 Base URL、Key、Model ID 三件套一起填只填 Base URL 必报 401。local proxy failed。现象是客户端报local proxy failed或连接被拒。根因通常是 qMLX 没起来或者端口被占。先确认进程lsof -i :8080 curl -s http://127.0.0.1:8080/v1/models如果 8080 被别的服务占了换--port 8081之类同时改客户端 Base URL。另一个常见原因是 macOS 防火墙拦了本地回环之外的绑定确认--host是127.0.0.1而不是0.0.0.0。reading choices 报错。现象是客户端解析响应时报reading choices或类似字段缺失。根因是服务端返回了错误结构通常是模型加载失败或请求体不合法。先直接 curl 一次看原始返回curl -s http://127.0.0.1:8080/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer local \ -d {model:qwen-122b,messages:[{role:user,content:hi}],max_tokens:4}如果返回里是{error: ...}按错误信息查模型路径和--max-context是否超过模型支持。如果返回正常但客户端仍报错多半是客户端把流式和非流式搞混了检查stream参数。OAuth 相关报错。现象是客户端提示 OAuth 失败或 token 过期。根因是你把本地 qMLX 当成了需要 OAuth 的云端服务。qMLX 是本地 OpenAI 兼容接口不走 OAuth。修法在客户端里关掉 OAuth 或登录流程选「自定义 OpenAI 兼容端点」填 Base URL 和 Key。如果你用的是 Codex 的auth.json把里面的 provider 指向本地端点别走官方登录。还有一个 qMLX 特有的坑restore_guard_enabled开着时某些本可成功的 restore 会被拒绝日志里出现 guard 相关提示。这是 alpha 阶段的已知问题guard 估计过于保守高估瞬时反量化占用又在物理上限下留很大 margin。临时修法是在配置里把它设为 false等估计改准再开。安全直觉是对的真放不下的 restore 该拒绝但现在的数字是在「狼来了」。6. 把 qMLX 接进日常编码工作流修完这三个 Bug、配好缓存之后Qwen 122B 在 Mac Studio 上已经足够快、足够稳可以每天做长上下文结对编程。我现在的用法是把它接进 Claude Code 和 ClineBase URL 指向本地 8080Model ID 用qwen-122bAPI Key 填local。这样代码和对话都不出机器没有限流也没有 API 账单。如果你也想复现这套配置建议按这个顺序来先把 qMLX 跑起来用 curl 确认缓存命中再把cache_config.json里的checkpoint_interval_tokens设为 0、eviction_policy设为unmatchable_first然后接客户端填全 Base URL、Key、Model ID 三件套最后用 metrics 端口盯住cache_hit和prefill_tokens确认每一轮都是 HIT 而不是 MISS。需要长期跑 Agent、或者想省掉自己维护推理栈的麻烦可以看看 Coding Plan它把这类长上下文编码场景的额度打包好了。想先验证模型对话效果模型对话页面可以直接试。要自己拿 Key 接进现有工具链API Keys 页面生成即可接入细节看接入文档。本地这套 qMLX 配置和云端额度并不冲突我通常是本地跑敏感代码、云端跑批量任务两边用同一套 OpenAI 兼容接口切换成本几乎为零。最后留一个我踩过的坑别把模型权重和 KV 缓存放同一个盘。权重读一次就常驻内存KV 缓存是高频随机读写两者抢 IO 会让 restore 变慢。我把权重放系统盘、KV 缓存单独挂一块 NVMe首 token 延迟又降了一截。这个改动不需要改任何配置只改disk_cache_dir的路径就行。