推理与性能模型架构

用 iPhone 给 Mac 加速大模型,看起来很疯,但又没那么疯

一个iPhone用10Gb/s的USB-C线挂在macbook旁边给本地大模型加速,第一眼看上去是种华而不实的geek toy。手机那点计算能力+一根物理线带来的通信延迟很容易杀鸡用牛刀。

我一开始看这个开源项目 backburner,也是这么想的。看了代码之后才发现很多人误解了。作者并不是想在手机上运行大模型。他始终在 Mac 上进行计算,并没有在手机上进行整个推理。

在这个系统中,MacBook Pro 是一个对外提供服务的宿主,上面跑着一个普通的本地服务端。比如一台 24 GB 内存的 Mac,读到长文本的时候内存吃紧,那部用线缆连着的 iPhone 就充当挂在线另一头的外部内存池和协处理器。它负责两个环节:一是在 prefill 阶段(读入长文本时)分担一部分网络层,二是在 Mac 装不下超长上下文时接管溢出的历史键值缓存。这两个概念你可能不陌生:prefill 是把整段输入一次读完,decode 是之后一个一个往外吐词。手机在这两个阶段里的角色完全不同,后面会看到。

即便你手边没有 iPhone 或内存小一点的电脑,这依然是一个有意思的场景。它描述的是许多人都可能遇到的困境:物理资源受限,但计算任务还要继续。长文本填满了主机的显存,外接设备的算力并不对称,二者仅以一根线相连。在这种限制下,下一步该怎么走?跳出具体机型,这种在物理约束中做取舍的思路,对多卡并行、分布式推理乃至边缘设备协作都同样适用:手头的资源有限,可任务必须完成,突破口在哪里?

先挑能在数学上无损切开的工作

人的本能会先去优化这根线——测通信带宽够不够,压协议延迟,把要发的数据高比压缩。但这样容易让人误以为只要数据跑得够快,两边的计算就能自然对接上。

干过分布式的人都知道,这里面有个最大坑。两台机器能不能一起做一件事,决定因素不是网线跑多快,而是你要交给它们做的计算有没有办法在数学上独立求解、最后还能无损合并。网线带宽只是第二步的事。

如果一个算子切分后仍要求大量的跨设备数据依赖,两头就需要同步中间状态。这种情况下即便线缆延时很低,往返的等待时间也可能抵消多设备分担算力带来的好处。这种算子不适合这里的切分方式。

在这个系统里,被选出来分流的计算是注意力。这是因为注意力有一个数学性质,就是它天然就可以做局部独立的计算。

当模型的全注意力层遇到长上下文时,它需要把当前的查询向量和历史上所有出现过的键做点积,再用注意力权重对值求加权和。文本越长,历史键值缓存就越大,最终会超出单机的内存容量。这时系统就让 Mac 把较早的那批键值缓存整体搬到手机上去托管,但前馈层和模型权重还是在 Mac 本地。

在进行注意力计算的过程中,Mac负责将当前的查询向量通过数据线发送给手机。手机则仅对其所存储的历史键值页进行局部注意力计算,得到一个局部结果,包括局部输出向量、局部最大值以及指数和。随后,手机将归一化后的局部输出向量和由最大值、指数和算出的对数归一化量返回给Mac。Mac利用对数求和指数公式,将手机的局部结果与本地的结果进行合并。

左侧是不可切分的算子,全局依赖导致两端频繁同步、收益被线缆延迟吃掉;右侧是可切的算子,按位置切开后各自独立求值、再精确合并

所以这样的分割,得到的结果和之前是一样的。从数学上讲,局部注意力合并起来是完全等价的,这种分割没有额外引入有损量化或者近似截断。在默认模式的贪婪解码测试下可以看到,生成的每一个词元都和单机逐字的结果完全一致。要想让两台设备合作,先把数学上能拆开的计算挑出来,比到处找更快的传输线实用多了。

状态组织的接缝决定了切分点

一旦确认了注意力算子可切分,下一步自然的直觉就是按两台设备的算力大小来分配计算量——主机强就多给几层,副机弱就少给几层,只要两边时间对上,吞吐就稳。

但一旦真的去切模型,往往会发现这样切非常不work。因为很多模型里面不同的层记东西的方法是不一样的。

比如这个项目用的 Qwen3.8-27B。它的大部分层不存每个历史词,而是记一个固定大小的摘要,新词来了滚动更新一下,前面信息被揉进摘要。这样省内存,但摘要就是一坨,没法把前半段历史抽出来给另一台机器,因为不再分了。摘要还往前滚,想回头只能从头算。

而另外的少数层用的另一种方法,给每个历史词单独存一行,攒成一张随上下文变长的表。这个表的每一行彼此独立,最早的几行可以整片挪走。这两类层在整个网络里交替出现:连续 4 层里,前 3 层是上面那种摘要层,第 4 层才是这种存表的层,然后 4 层一循环,一直到模型末尾。

这直接决定手机能干啥。第一种层只有一个整块的摘要,要给手机用只能把整层挪过去,没法按位切。第二种层有一张表,可以把表里最老的几行挪到手机内存里留着,位置随便切。于是系统的整层切分点必须对齐每 4 层一次的循环边界,顺着数据自己的接缝来。能切哪儿,受这两类层的排列约束,具体切在哪个边界仍由工程师选择。

切好之后,在默认配置不超过 64k 上下文的 split prefill 中,Mac 跑前 40 层,把中间结果扔到手机继续算后面的 24 层;每批最后一个 ubatch 则全部留在 Mac 上跑。看到这儿容易犯嘀咕:Mac 先算完 40 层再交给手机,这不还是你先我后,能快在哪?诀窍是流水线。长输入本来就切成一批批小块送进来,Mac 算完一块的前 40 层,不等手机,把中间结果发过去就转头去算下一块的头 40 层。手机还在啃第一块的尾巴,Mac 已经进了第二块。两台设备在同一段时间里各忙各的,前后依赖被流水线盖住一部分,等待就减少了。

把不再改动的数据编译成硬件算力

做完状态切分,系统立刻被另一个端侧推理的常见瓶颈卡住,它在 decode 阶段。在全注意力层,每个词元出来,芯片都要读取之前的键值缓存,越长越贵,内存带宽就可能成为瓶颈。

对这个瓶颈,一般的思路是优化缓存的布局,或者对内存传输做一些小改动。但仔细看自回归推理的过程,就会发现一个容易被忽视的情况:那些几十步之前生成的历史键值,在整个 decode 中只会被频繁读取而不会被修改。既然这些数据已经固定,还一直把它们当动态数据从内存里读,就非常不划算。

开发团队基于这个洞察换了一种方案。手机里有个 Apple 神经引擎(ANE),它可以运行以固定权重做矩阵乘法的模型。于是系统把副设备持有的历史键值页,一页一页地直接编译成一个 ANE 模型文件,这些页面在 prefill 阶段就陆续在后台编译,详见 神经引擎实现文档。编译好的模型里,原本在内存里的键值就直接固化成了静态权重。

等到 decode 时主机发来新的查询向量,副设备把查询向量作为输入送进这个 ANE 模型,由它计算已编译页面的局部注意力结果。查询向量每步更新,已编译页面的历史键值作为模型权重保持不变,并不是不再需要内存访问。

从冻结历史页、编译为硬件权重、输入查询、硬件矩阵乘到结果一致的一条链路;对比传统路径反复读取内存总线、受限于带宽

这个把历史缓存搬上手机、并且用手机分担旧上下文的注意力机制,在项目的设计说明里有更详细的解释。实测发现,在 A18 的旧页注意力算子测试中,这种把静态数据编译成硬件权重的做法比手机的 GPU 通用图形处理器快 2-3 倍。页权重采用 fp16,没有采用精度不足的 int8 页权重;在 51k 真实会话测试中,33 个词元与精确路径全部一致。数据不变,就把它交给硬件当权重去算,旧页注意力计算就这么得到加速了。

面对狭窄链路先做传输减法

即使确定了数据的表示形式和算子的划分方法,两个设备之间仍然需要通过一条链路进行通信。主机设备计算速度快,但副设备相对较慢,这条链路的任何不必要延迟都会导致整个流水线的速度下降。

很多分布式系统遇到这种情况,就会在通信层不断加戏,堆调度协议、堆多路复用、堆重试机制,用更精巧的管道工程去应付窄带互联。这套系统反其道而行之:线窄,那就在架构上做减法,让绝大多数数据根本不用过线。

它在线缆两端做了三条减法。

首先,在手机端镜像自己那些网络层的键值缓存和循环状态,跨线同步的只有新增加的几行。

第二,层间只传残差。这里不传输层内中间张量,而是传输层间残差向量。

第三,在 split prefill 模式下,终点计算在本地。每批输入的最后一个 ubatch 强制在Mac上本地运行。这样,词表维度巨大的 logits 张量留在主机上,无需过线。

主机前序层与手机后序层的时间轴:大部分工作留在各自本地闭环,只有新增增量、层间残差等很少的数据穿过线缆,两端的计算块在时间上重叠把传输藏起来

在大幅降低跨线数据量后,又加入了时间上的流水线:超过 64k 后,512-token ubatch 交错成两半,Mac 负责一半词元的全部网络层计算,手机同时计算另一半词元对旧键值的注意力。两者在时间上交错。这样在 140k 上下文测试中,prefill 阶段吞吐从 58 提升至 68 tok/s,约提升 17%;约三成的提升则来自 32k 和 48k 的 split prefill 测试。窄链路上,花时间搭管道收益有限,更管用的是靠本地闭环削减大部分跨线交互。

舍弃负优化与看清真实的收益边界

观察一个系统设计得怎么样,不能只看它实现了什么功能,还要看它在实验中发现并抛弃了什么。在纸上谈兵的时候,很容易构想一些看似合理的协作模式,比如 Mac 自己有 ANE,为什么不利用它在 decode 的时候也加入计算?手机里面有 GPU 也有 ANE,为什么不把两者都利用起来?但真正到了硬件上面,往往会发现这些设想是错误的。项目记录了几种经测量后禁用或搁置的方案:

早期他们尝试过用 Mac 自己的 ANE 来帮忙 decode,但测量后发现生成速度反而慢了 26%,因为 ANE 在频繁读写内存时和图形核心抢总线,拖慢了主计算通路。这个想法也被删。

手机端也试验过让图形核心和 ANE 一起跑 prefill。两者都忙起来会打架,ANE 的吞吐直接掉很多,两者合起来只有单方的 1.1-1.25 倍,所以方案被搁置了。

他们也尝试过在运行时动态学习 ANE 和图形核心的分片比例。但 ANE 空闲时会降频,探测请求的耗时忽高忽低,把在线调度逻辑搅乱了,最后还是换回了一个固定的静态常数。

除了去除反例之外,这个系统非常坦诚地说明了它的局限性。在默认模式下,对于 64k 以下的中短文本,它只依赖 Mac 进行 decode,因为此时副设备没有参与 decode,也就谈不上加速。而对于不超过约 512 个词元的轻量读取请求,则干脆不启用 split prefill,直接在主机上单机处理,以避免通信成本不划算。同时,该系统对使用场景也有要求,比如在移动操作系统中,由于系统不允许 app 在后台使用 GPU,因此如果手机参与计算的话需要前台亮屏。此外,整个系统目前也只支持单请求串行。

这整套设计真正解决的是从跑不起来到能稳定跑起来,而不是把常规推理速度翻番。一个 8GB 内存的主机装不下项目使用的 27B 模型,24 GB 主机在原有配置下也装不下超出容量的长文本缓存;用一根线把外部设备拉进来协同,这道门槛被推开了。把不可切分的工作挡在外面,顺着数据结构的接缝划分流水线,把凝固的数据转成前向算力,在窄链路上做足减法,这套在极端限制里逼出来的决策路径,即便拿掉线缆和具体设备,遇到资源瓶颈时照样能用。

评测维度 场景与配置基准 实测表现或测量基线
长文本预填吞吐提升 16k / 32k / 48k 上下文 单机 109 / 101 / 87 tok/s 提升至协同 157 / 130 / 113 tok/s
冷启动大文本首字响应 27k 词元会话首次应答 原版 245 秒,单机优化 228 秒,接入协同加速降至 168 秒
专用硬件算子求值耗时 每 16k 键值页(4 个头) 耗时 1.85 毫秒(折合每 1k 键值 0.113 毫秒),达通用核心 2-3 倍
超长上下文单步耗时 140k 极端深度逐字生成 纯通用计算 279 毫秒/词元,按层固化 2 页降至 208 毫秒,3 页降至 176 毫秒
物理线缆交互延迟基准 经手机 app 的 USB-C 16 字节请求/回复往返时间 中位数 96 微秒,90 分位值 183 微秒
极限受限场景从零到一 8 GB MacBook Neo + iPhone Air,以 split decode 运行 27B 模型 使原本装不下的模型能运行,实现协同解码 3.9 tok/s 与预填 64 tok/s

现在大语言模型的架构正在变得越来越混合注意力,那系统状态里面可切分和不可切分的比例也在发生着变化,未来的模型会有更多定长的循环状态,更少的注意力层,但是每个注意力层的计算会更重,这又会给主从计算节点之间带来什么样的切分界限呢?

鸭哥每日手记

日更的深度AI新闻和分析