修三个bug,让Qwen 3.5 122B在Mac Studio上跑成日常主力
买了台 Mac Studio Ultra,想跑个大模型。结果呢?
Andryo Marzuki 在陪产假期间冲动下单了一台 M3 Mac Studio Ultra(96GB 统一内存),目标很简单:把最前沿的模型跑在本地,保持常驻内存,拥有一个真正理解上下文的对话式 AI。但几周用下来,他发现”模型能装进内存”和”模型能日常用”之间,隔着好几个深坑。
这个故事不是跑个分、发个图就完事。它讲的是一个人怎么从 DS4 Flash 换到 Qwen 3.5 122B,又怎么在 serving stack 里连杀三个 bug,才把长上下文 agentic coding 从”等一杯咖啡”变成”秒回”。
为什么换掉 DS4 Flash?不是它差,是不合适

DS4 Flash 是 antirez 的实验项目,能把大模型塞进消费级硬件。Marzuki 一开始就用的它,而且承认它工程上很精彩。但对他的工作流来说,有个致命问题:上下文超过 5 万 token 后,每次追问都要等 3 到 5 分钟才出第一个 token。
注意,不是出完整答案,是第一个 token。这种延迟在 pair programming 里无法接受。模型还在 prefill,人已经去看下一个问题了。
于是他转向 Qwen 3.5 122B。这是一个 122B 参数的 MoE 模型,每次前向大约只激活 100 亿参数。对 M3 Ultra 的内存带宽来说,这个 active-param 规模刚好落在甜点区:GPU 喂得饱计算单元,不会 stall。同时 96GB 统一内存还能给 deep KV cache 留出头 room,让长上下文不必频繁换页。
真正的硬仗:三个藏在缓存里的 bug

换成 Qwen 只是第一步。真正让模型可用的,是修复 rapid-mlx 分支里三个彼此掩盖的 bug。他把修复后的 fork 叫做 qMLX。
问题根源在于 hybrid attention 架构:GatedDeltaNet(SSM)层和 dense attention 层混在一起。SSM 的循环状态不能回退或修剪,所以内存中的 prefix cache 只要包含这些层就会被整体丢弃。实测里,内存缓存命中率是零,109 次全是磁盘命中。换句话说,磁盘 KV restore 不是后备方案,而是整套缓存系统的全部。
第一个 bug 是系统提示里的时间戳。KV 复用是字节级精确匹配。只要 prompt 有一处不同,从那里开始全部重算。而 agent 框架每次都在系统提示里塞一个唯一 message ID,位置又很靠前。结果就是 13 万 token 的 prompt 每轮都从前端开始冷启动。修复方法很简单:删掉那行。这个 ID 没人读回去,应该放在每轮会变的那部分 prompt 里,而不是缓存前缀里。
第二个 bug 在打断路径上。模型还在回答时,用户发新消息,agent 会打断当前生成。这本身是对的,但实现上它直接 break 出循环,没有保存已经流式输出的回复。推理服务器已经把那些 token 写进 KV cache,对话历史里却少了 assistant 这一轮。下一次 prompt 就在一个”缺了回复”的状态上继续,缓存自然从深处断开。修复方式是在退出前把已生成的回复持久化,和断网重连时的恢复逻辑一样。
第三个 bug 更隐蔽:checkpoint store 里有两种写入。一种是正常 checkpoint,带 token key,下一轮能恢复。另一种是后台每生成 256 个 token 就写一次的”全量快照”,不带 key,永远匹配不上,却占着磁盘配额。长工具调用场景会触发大量这种垃圾写入,把 oldest-first 的淘汰策略挤爆,连唯一有用的 checkpoint 也被一起清掉。修复是优先淘汰无 key 的 junk checkpoint,并在需要 restore 时直接关掉后台全量写入。
修好之后的体验,完全是两回事

同一串对话,以前每轮都冷填 3 万 token,修复后从 3 万增长到 5 万多 token,每一轮几乎都是 cache hit:prefill 只剩几十到一千多个新 token,耗时从分钟降到秒级。
Marzuki 还做了一个诚实对比:同样 32k token 的重复 prompt,不开缓存每次 prefill 要 88 秒,开缓存只要 0.64 秒。这个差距会随上下文长度放大。缓存不是锦上添花,而是让长上下文对话成立的前提。
他特别强调数字不能骗人。很多 benchmark 会把 prefill token 和 decode token 混在一起算”总吞吐”,这样你发一个超长 prompt 就能让数字很好看,但用户实际感受到的回复速度完全没变。所以他坚持把 prefill 和 decode 分开测。在 M3 Ultra 上,短上下文 decode 大约 55 tok/s,64k 上下文还能维持在 28 tok/s 左右。q4 量化约 27 tok/s,q8 约 15 tok/s。
这个解码曲线本身也说明 hybrid attention 的价值:75% 的 DeltaNet 层携带固定大小的循环状态,不随上下文变慢;只有 25% 的 dense attention 层会随 KV 增长减速。所以上下文从 1k 涨到 64k,decode 速度只是温和地下降约 2 倍,而不是断崖式崩塌。
这对你我有什么意义

qMLX 现在已经开源在 GitHub 上,但它不是魔法。Marzuki 自己也说清楚:它只在 M3 Ultra 96GB 这台机器上测过,没有跑过减配版 Ultra,也没有跑过其他内存配置。里面甚至还有一个过于保守的内存 guard,默认被他关掉了,因为会误报可用空间。
真正值得记住的是他修 bug 的思路:本地大模型不是模型能跑就行,而是缓存、恢复、淘汰、打断每一条路径都要稳定。只要 prompt 里有一点点每轮都变的东西,只要打断时少写一轮回复,只要磁盘里混进无法恢复的 checkpoint,用户体验就会从”接近零延迟”跌回”泡杯咖啡等结果”。
如果你也在 Mac 上跑 Qwen 3.5 或类似的 hybrid MoE,并且被 cold prefill 折磨,不妨先看看这三个地方。很可能你已经踩到了同样的坑,只是还没意识到是缓存出了问题。
来源:mrzk.io
