苹果芯片推理优化一盘散沙,社区应集中整合而非另起炉灶
这是一篇手写的帖子。我花了太多时间试图在 Apple Silicon 上获得快速推理。这篇文章写给那些想知道在 Mac 上运行本地模型的最新进展,以及为什么自己看到的性能与社区其他人宣称的不一致的人。
TL;DR
过去两周我全职投入研究 Apple Silicon 上的推理优化现状,老实说,软件栈一团糟。目前没有任何框架能像 CUDA/NVIDIA 那样,为最新的 Qwen 模型提供完整的推理优化:前缀缓存、投机解码、分页 KV 缓存、连续批处理、动态调度、flash attention 等等。
在 CUDA/NVIDIA 上,这些东西很多已经成熟,被整合进人们实际使用的推理栈。在 Apple Silicon 上,这些部件散落在 mlx-lm、vllm-metal、各种 fork、自定义模型转换以及其他一大堆框架里,其中很多只实现了整个栈的一部分。
我发现最大的问题是,较新的 Qwen 模型采用混合 KV/循环状态架构,这使得前缀缓存和投机解码更难组合。此外,mlx-lm 目前在模型转换时会丢弃内置的 MTP 头,因此即使模型本身支持内置投机解码,转换后也缺少你需要的东西。
从我测试过的所有项目来看,vllm-metal 是当前最接近完整 Apple Silicon 推理优化栈的。我认为我们不应该每次缺什么就再搞一个 fork,而应该先把一个栈做好,然后把各个部分合并回 mlx-lm 和 vllm 的上游。
长文版本
我花了两周时间深挖这些。我本不应该花这么长时间去理解 Apple Silicon 上的推理优化领域,我觉得这正说明这个领域现在有多糟糕。首先,llama.cpp 在 CUDA 上所有功能都内置且可用。
你上 Reddit,打开 LocalLLaMA,心想,“好吧,我怎么在 Mac 上跑这个?”然后突然冒出一千种不同的项目,个个都说自己最快。你看着这些东西,心想:这他妈怎么回事?这些都是什么?我为什么需要所有这些?
先是你听说“大家都在用 LM Studio”。于是你试试 LM Studio,然后觉得,嗯,不太对劲。有点慢,有些毛病,算了。于是你开始深挖……你一个框架一个框架地试,一个测试接一个测试,想弄清楚为什么你 Mac 上的推理性能永远达不到别人说的那样。这正是我的经历,我希望这篇文章能对清理这堆烂摊子有所帮助。
关于推理优化的一些“背景”
给不熟悉的人解释一下:本地推理基本上有两个主要阶段:预填充(prefill)和解码(decode)。
预填充就是模型处理你给出的上下文并填满 KV 缓存。比如你向服务器发送一个 1 万 token 的提示词,它处理所有这些文本,让模型进入该对话所代表的状态。
然后进入解码模式。通常是自回归式的,也就是逐个 token 串行预测,直到生成完整个回复。
这两个阶段周围有大量优化技术:前缀缓存、分页 KV 缓存、投机解码、flash attention、flash decoding、连续批处理、动态调度等等。这些东西在不同场景下各有作用。
如果你运行一个庞大的多用户推理服务器,某些优化显然比一个人坐在那里和模型对话更重要。但对于我说讨论的本地、长会话、智能体式用例,我认为其中最重要的两项是前缀缓存和投机解码。如果这两个都没有,我保证你会对 Mac 上本地模型的性能感到失望。前缀缓存优化预填充端,投机解码优化解码端。
前缀缓存
记住,你的服务器是无状态的,所以如果你在进行一个很长的智能体对话,上下文不断变大,每个请求通常都意味着模型必须重新处理整个对话,然后才能开始生成下一个回复。
假设你有 1 万 token 的上下文,你发送一条消息,模型处理这 1 万 token,进行一些推理,发出回复。然后你再发一条消息,现在你有 1.05 万 token。没有前缀缓存,它就得一遍又一遍地处理全部内容。显然,随着对话变长,情况会越来越糟。
所以你可能看到基准测试写着 45 token/秒,但当你真的在长时间运行的会话中使用它时,突然只有 9 token/秒。如果出现这种情况,很可能是框架没有正确实现前缀缓存,所以你反复付出预填充的代价。很多框架会告诉你,是的,前缀缓存已经实现了,但这里有一个巨大的、他妈的微妙之处。
Qwen 问题
现在人人都想跑的模型,比如 Qwen3.8,并非只使用普通 KV 缓存。它们有一个混合缓存架构:除了普通 KV 缓存,还有一个循环状态(recurrent state),所以它是个混合型 Gated DeltaNet/GDN 模型(是的,这是个兔子洞)。
这个循环状态很棘手,因为它不像扁平的 KV 缓存那样工作。可以把 KV 缓存理解为保存历史,而循环状态更像是模型的“当前”状态,它会随着你处理 token 而覆盖之前的状态。这就是这些模型难以实现前缀缓存的原因。
vllm-metal 最近(就在上周)为这些混合模型添加了前缀缓存支持,但这项支持有一个相当大的限制:你必须在前缀缓存和投机解码之间二选一,不能两者兼得。
他们把功能做到这个程度,是因为实现起来确实困难,他们需要更多时间来深入研究。
投机解码
投机解码有很多种实现方式,每年都有更多白皮书提出新方法。其中一些实现使用一个单独的模型,称为草稿模型(draft model),很多 MLX 引擎 fork 都在采用这个方案(具体原因稍后解释)。DFlash 就是最近发表的草稿模型想法的一个实例。可以想象,每种实现都有利弊。草稿模型实现的缺点是占用内存大得多,而且更容易预测错接下来的 token。
投机解码的另一种实现是多 token 预测(multi-token prediction),简称 MTP。这种实现方式下,模型在训练时就内置了投机解码能力。可以想象,这种实现有不少好处。Qwen3.6/3.8 有内置的 MTP 头,你可以在它们的 HuggingFace 页面上看到。
如果还不清楚的话,投机解码意味着你在解码时预测未来多个 token。比如你可以一次性猜接下来三个 token,而不是通常的一个,其中有 2/3 可能猜对了。
这就回到了人人都想跑的 Qwen 模型,如我们所知,它们内置了 MTP。在 llama.cpp 上加载 Qwen3.8-27B,这就能直接工作。在 mlx-lm 上则不行。为什么?有两个原因。
首先,如我之前所说,这些 Qwen 模型有循环状态,它本质上会“丢失”历史,所以现在你需要把模型回滚到预测出错 token 之前的状态,才能继续前进。
对于普通 KV 缓存,这相对直接,因为可以认为之前的状态都是可用的。但对于循环状态,你必须真正恢复之前的循环状态,这意味着需要一堆代码/逻辑来保存快照并从中恢复。这不是小事。
更大的问题:mlx-lm 缺乏支持
我认为很多框架碎片化问题正是从这里来的。mlx-lm 基本上是 Apple 提供的基础层,整个 Apple Silicon 推理生态系统都建立在它之上。
关于 MLX 模型:模型通常以 SafeTensors 格式发布在 HuggingFace 上,mlx-lm 提供转换工具将它们转换成 MLX 模型。问题在于,就 mlx-lm 当前的 main/master 状态而言,运行转换时,它会悄悄移除 MTP 权重和头部。这在任何 mlx-community 的 model.safetensors.index.json 文件中都能看到。AirRunner 已经提交了一个 PR,几个月来一直试图为 mlx-lm 添加这个支持,但至今仍未合并,因为维护者 AFK 了。
mlx-lm 的这种状态意味着,在 Apple Silicon 上,这些 Qwen 模型如果不做额外修改,就都被阉割了 MTP 能力。这就是为什么你在 HuggingFace 上看到那么多不同版本的 MLX 模型,在 GitHub 上看到那么多不同版本的推理引擎。现在我们每周都会在 LocalLLaMA 上看到一篇新帖子,介绍又一个“BlAzInGlY F4sT”的框架。
随便挑一个你能想到的 MLX 推理项目跑跑看。对于 Qwen3.5+,它要么不真正支持前缀缓存,要么不支持内置投机解码。请忽略那些愚蠢的基准测试,实际用你的代码库跑几轮对话,你就会看到结果。
回到我之前关于推理优化的观点:如果 macos 上这两个不能同时正常工作,你不会对性能满意。要么只有投机解码,基准测试看起来很惊艳,但智能体性能显著下降;要么前缀缓存正常工作(目前是 vllm-metal),上下文窗口内 tps 稳定,但太慢,没什么用。
所以,我们现在身处何处?
我测试了人们讨论的大多数主要项目。我在 Apple Silicon 上试过 llama.cpp,性能对我来说就是不行。我还注意到我的笔记本运行它时明显更加吃力、温度更高。我知道它底层用的是 MPS,但我认为 mlx-lm 有 Apple 特有的优化,llama.cpp 没有同样的优化。所以至少从我的测试来看,我认为 llama.cpp 目前不适合作为 Apple Silicon 的推理环境。
我发现最成熟、打磨得最好的是 vllm-metal,考虑到 vllm 出自 UC Berkeley,这也说得通。它已经内置了很多推理优化:连续批处理、动态调度、分页 KV 缓存、flash attention、前缀缓存等等。
问题是,我们又回到了同一问题上。它支持前缀缓存,也支持投机解码。但由于混合循环状态问题难以解决,目前在这些模型上你无法同时拥有两者。
那我们究竟该怎么做?
拜托,别再搞新框架了。在 MTP 支持被合并进 mlx-lm 上游之前,我认为我们应该基于 AirRunner 的工作维护一个社区 fork,大家就都用这个。与其每次发现缺什么功能就有人用 Claude 写个新包装器,不如把这些精力投入到同一个代码库中。
把内置 MTP 支持做好,为混合循环状态实现真正的前缀缓存,让前缀缓存和投机解码能协同工作,同时保留那些已经能用的东西,比如连续批处理、动态调度、分页 KV 缓存、flash attention 等。我们不需要为此再搞一个 fork。
是的,我们可以实验 D-Flash 和其他投机解码方法,我不是说不要做这些,但我们需要先有一个良好的基线。目前的 Apple Silicon 推理优化领域,感觉就像每个人都拼好了拼图的一块,然后决定围绕自己那块做一个他自己的、该死的拼图盒。
我不想花两周时间去搞清楚哪个 fork 包含哪个 PR、哪个模型转换保留了哪些权重、“投机解码”指的是 MTP 还是单独的草稿模型,以及当我的上下文变长后前缀缓存是否真的有效。
截至 2026 年 8 月 15 日,经过以上所有测试,我仍然没有在 Apple Silicon 上找到能与 CUDA/NVIDIA 提供的整体推理优化栈相媲美的东西。
所以我对社区的请求是:大家都别再造 fork 了,就老老实实去修补那一两个关键项目吧。希望这篇文章能给那些和我两周前一样困惑的人一盏指路明灯。